ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

XML Web Service并发异步调用的问题及其解决方案

XML Web Service并发异步调用的问题及其解决方案 最近在忙些其他项目的事情之前的MOSS 2010开发系列暂时搁一搁了。这个项目是传统的C/S架构有客户端有服务器。客户那边的环境还比较老是Windows Server 2000. 很不幸的是Windows Server 2000是不可以安装.NET Framework 3.5的这个问题我之前也提到过所以其实有很多东西得用比较早的技术去实现。一个典型的例子就是在服务层的设计里面我被迫放弃了WCF而是采用了XML Web Service的方式。当然咱对Web Service很熟悉也很亲切。这不存在什么问题。今天要谈一个小问题也是我们总结归纳出来在使用XML Web Service的场景下如果需要并发地发起异步调用而且是共享一个代理类的实例的话可能出现的一些问题及其解决方法。记得很早之前某同学也跟我说到过这个问题今天还是总结一下吧闲话少说我们来看一个例子吧1. 采用Web Service的标准做法创建Web Serviceusing System.Web.Services;using System.Threading;namespace WebApplication1{/// summary/// Summary description for WebService1/// /summary[WebService(Namespace http://tempuri.org/)][WebServiceBinding(ConformsTo WsiProfiles.BasicProfile1_1)][System.ComponentModel.ToolboxItem(false)]// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.// [System.Web.Script.Services.ScriptService]public class WebService1 : System.Web.Services.WebService{[WebMethod]public string HelloWorld(){Thread.Sleep(10 * 1000);return Hello World;}}}【注意】为了做异步调用的演示我这里简单地让该方法休眠了10秒钟后返回。创建客户端代理image创建简单的界面来实施调用image编写如下简单的代码using System;using System.Windows.Forms;namespace WindowsFormsApplication1{public partial class Form1 : Form{public Form1(){InitializeComponent();}localhost.WebService1 proxy new localhost.WebService1();private void button1_Click(object sender, EventArgs e){proxy.HelloWorldCompleted (obj, a) {MessageBox.Show(a.Result);};proxy.HelloWorldAsync();}private void button2_Click(object sender, EventArgs e){proxy.HelloWorldCompleted (obj, a) {MessageBox.Show(a.Result);};proxy.HelloWorldAsync();}}}【注意】这里两个按钮的事件是一模一样的。因为在一个窗体里面我们当然希望proxy是共用的推而广之甚至在整个应用程序都只有一个实例。然后在每个方法中调用它的一些方法。代码看起来很合理不是吗那么我们来看一下运行起来会怎么样Oooop,我们收到了一个错误imageimage一堆让你看得头疼甚至有些不知所云的错误消息对吧其实简单来说就是XML Web Service的机制不允许在一个异步请求没有返回之前再发起另外一个异步请求。这个其实与AJAX里面的机制有相似的。anyway我们如何解决这个问题呢2.采用多个proxy实例的做法来解决大家很容易想到的一个做法是既然一个Proxy不能同时发起多个异步调用那么创建多个不就可以了么using System;using System.Windows.Forms;namespace WindowsFormsApplication1{public partial class Form1 : Form{public Form1(){InitializeComponent();}private void button1_Click(object sender, EventArgs e){localhost.WebService1 proxy new localhost.WebService1();proxy.HelloWorldCompleted (obj, a) {MessageBox.Show(a.Result);};proxy.HelloWorldAsync();}private void button2_Click(object sender, EventArgs e){localhost.WebService1 proxy new localhost.WebService1();proxy.HelloWorldCompleted (obj, a) {MessageBox.Show(a.Result);};proxy.HelloWorldAsync();}}}谁说不是呢你当然可以像上面这样做而且我可以担保它不会再报告那个错误啦。这里唯一的问题就是我们创建了多个Proxy的实例。从一个严肃的程序员的角度来说似乎这是不可以接受的。创建新的对象实例毕竟需要成本。3. 使用ThreadPool发起同步的调用让我们来思考一下这个问题它的本质是如果你采用异步调用则无法同时发起多个请求对同一个proxy而言。那么如果是同步调用的话就自然没有这个问题了吧。但是如果同步调用的话却又会阻塞线程导致你根本无法发起第二个请求。这有何难呢我们大不了可以开两个线程然后让这两个线程同时工作在它们上面运行各自的同步调用方法不就可以了么为了让大家更好地理解我用最简单的做法来实现吧ThreadPool是一个静态类顾名思义它代表了线程池的概念。对于初学者而言创建和控制线程并非易事。所以.NET提供了这个ThreadPool实际上相当于是傻瓜式的多线程多任务的接口。using System;using System.Windows.Forms;using System.Threading;namespace WindowsFormsApplication1{public partial class Form1 : Form{public Form1(){InitializeComponent();}localhost.WebService1 proxy new localhost.WebService1();private void button1_Click(object sender, EventArgs e){ThreadPool.QueueUserWorkItem((obj) {var result proxy.HelloWorld();MessageBox.Show(result);});}private void button2_Click(object sender, EventArgs e){ThreadPool.QueueUserWorkItem((obj) {var result proxy.HelloWorld();MessageBox.Show(result);});}}}ThreadPool类型有一个静态方法QueueUserWorkItem顾名思义就是将用户任务排进队列等待执行。你可能会说那么这是要排队么当然但是既然称为Pool也即是池就自然不止一个线程它里面有好多个线程可以使用自然就可以同时运行多个任务了。关于这一点大家可以类比一下我们在超市买完东西之后去收银台付费的情况。很多大超市都有声势浩大的一排收银台【注意】要实现多线程还有很多做法例如下面这样无疑也是可以的using System;using System.Windows.Forms;using System.Threading;namespace WindowsFormsApplication1{public partial class Form1 : Form{public Form1(){InitializeComponent();}localhost.WebService1 proxy new localhost.WebService1();private void button1_Click(object sender, EventArgs e){(new Thread((obj) {var result proxy.HelloWorld();MessageBox.Show(result);})).Start();}private void button2_Click(object sender, EventArgs e){(new Thread((obj) {var result proxy.HelloWorld();MessageBox.Show(result);})).Start();}}}这个做法是由我们自己创建并启动了一个线程。这看起来虽然比较刺激但并见得是多么好的一个做法。尤其是你对线程操作并不是特别熟悉的情况下我推荐你还是多用ThreadPool至于其他的做法如Backgroundworker以及自定义Callback等这里就不列举了。4. 线程安全问题这样做看起来是解决了问题了。但是大家要知道通常情况下我们希望调用Web Service获得的结果能进一步地进行处理或者要显示在界面上。例如下面这样using System;using System.Windows.Forms;using System.Threading;namespace WindowsFormsApplication1{public partial class Form1 : Form{public Form1(){InitializeComponent();}localhost.WebService1 proxy new localhost.WebService1();private void button1_Click(object sender, EventArgs e){ThreadPool.QueueUserWorkItem((obj) {var result proxy.HelloWorld();this.Text result;//将结果显示在窗体的标题上});}private void button2_Click(object sender, EventArgs e){ThreadPool.QueueUserWorkItem((obj) {var result proxy.HelloWorld();this.Text result;});}}}再次运行这个代码就会发现它又报错了这回报告的错误是下面这样的image这个错误的意思是说当前的线程需要访问控件”Form1”但是Form1并不是它所创建的。什么是当前线程就是我们用ThreadPool发起的一个任务所占用的线程。而Form1这个控件是谁创建的呢是主线程创建的。这就是我们常说的线程安全性问题也就是说在多个不同的线程之间是不能互相访问到一些资源的。还是用超市的收银做例子如果我是排在1号收银台那么2号收银员是不可以把我的钱拿走的。那么如何解决这个问题呢其实这还是比较简单的Windows Forms(或者WPF)都内置了一些机制来解决using System;using System.Windows.Forms;using System.Threading;namespace WindowsFormsApplication1{public partial class Form1 : Form{public Form1(){InitializeComponent();}localhost.WebService1 proxy new localhost.WebService1();private void button1_Click(object sender, EventArgs e){ThreadPool.QueueUserWorkItem((obj) {var result proxy.HelloWorld();this.BeginInvoke(new Action(() {this.Text result;//将结果显示在窗体的标题上}));});}private void button2_Click(object sender, EventArgs e){ThreadPool.QueueUserWorkItem((obj) {var result proxy.HelloWorld();this.BeginInvoke(new Action(() {this.Text result;//将结果显示在窗体的标题上}));});}}}【注意】如果是在WPF中则是用Dispatcher.BeginInvoke5. WCF中是否有这个问题有些朋友可能会接着问道那么这个问题是否在WCF中也存在呢我们还是以事实为依据看一个例子吧创建一个WCF服务服务契约using System.ServiceModel;namespace WebApplication1{// NOTE: You can use the Rename command on the Refactor menu to change the interface name IService1 in both code and config file together.[ServiceContract]public interface IService1{[OperationContract]string HelloWorld();}}服务实现using System.Threading;namespace WebApplication1{// NOTE: You can use the Rename command on the Refactor menu to change the class name Service1 in code, svc and config file together.public class Service1 : IService1{#region IService1 Memberspublic string HelloWorld(){Thread.Sleep(10 * 1000);return Hello,world;}#endregion}}添加服务引用image【注意】这里不要着急点击“Ok”而是点击“Advanced…”image勾选上Generate asynchronous operations也就是说生成异步的操作。因为WCF服务引用默认是不生成异步操作的using System;using System.Windows.Forms;using System.Threading;namespace WindowsFormsApplication1{public partial class Form1 : Form{public Form1(){InitializeComponent();}ServiceReference1.Service1Client proxy new ServiceReference1.Service1Client();private void button1_Click(object sender, EventArgs e){proxy.HelloWorldCompleted (obj, a) {MessageBox.Show(a.Result);};proxy.HelloWorldAsync();}private void button2_Click(object sender, EventArgs e){proxy.HelloWorldCompleted (obj, a) {MessageBox.Show(a.Result);};proxy.HelloWorldAsync();}}}我们现在发现两次调用并不会触发任何的错误。看起来WCF是允许并发异步调用的。但是令人惊讶的事情再次发生了就是虽然这两次调用都会返回结果但那个MessageBox.Show方法却会被执行四次。两次调用会导致四次的返回或者叫后续处理这又是为什么呢这个问题就留给读者自己琢磨一下吧【注意】如果有10个请求同时发出那么会有多少该处理呢10的平方也就是100次。所以WCF的机制其实并没有本质上的改变。遇到需要并发异步调用远端服务这种需求我还是推荐你用ThreadPool直接发起同步调用吧。这就是本文的结论。
返回列表