回调与并发: 通过实例剖析WCF基于ConcurrencyMode.Reentrant模式下的并发控制机制

来源:互联网 发布:天书世界武将转生数据 编辑:程序博客网 时间:2024/05/16 10:14

http://www.cnblogs.com/artech/archive/2010/03/31/1701660.html

对于正常的服务调用,从客户端发送到服务端的请求消息最终会被WCF服务运行时分发到相应的封装了服务实例的InstanceContext上。而在回调场景中,我们同样将回调对象封装到InstanceContext对象,并将其封送到客户端。当服务操作过程中执行回调操作的时候,回调消息最终也是分发到位于客户端封装回调对象的InstanceContext。从消息分发与并发处理的机制来看,这两种请求并没有本质的不同。接下来,我们通过《实践重于理论》中的实例,综合分析WCF对并发服务调用和并发回调的处理机制。

一、将实例改成支持回调的形式

为此,我们需要对我们上面给出的监控程序进行相应的修改。首先需要修改的是服务契约ICalculator。服务契约ICalculator的Add操作接受传入的操作数并以返回值得形式返回到客户端。现在我们通过回调的形式来重写计算服务:将Add的返回类型改称void,计算结果通过执行回调操作的形式在客户端显示。

   1: [ServiceContract(Namespace="http://www.artech.com/",CallbackContract =typeof(ICalculatorCallback))]
   2: public interface ICalculator
   3: {
   4:     [OperationContract]
   5:     void Add(double x, double y);
   6: }

作为回调契约的ICalculatorCallback接口定义如下,计算结果传入ShowResult方法显示出来。在一般情况下,我们会将Add和ShowResult和操作定义在单向(One-way),但是这里我并没有这么做,所以无论是服务操作Add还是回调操作ShowResult均采用请求/回复消息交换模式。

   1: using System.ServiceModel;
   2: namespace Artech.ConcurrentServiceInvocation.Service.Interface
   3: {
   4:     [ServiceContract(Namespace = "http://www.artech.com/")]
   5:     public interface ICalculatorCallback
   6:     {
   7:         [OperationContract]
   8:         void ShowResult(double result);
   9:     }
  10: }

在本例中我们的CalculatorService采用单例实例上下文模式(InstanceContextMode.Single)。为了能够执行回调,将并发模式设置成ConcurrencyMode.Reentrant。在Add操作中,我们可以将整个执行过程分成三个阶段:PreCallback、Callback和PostCallback,而且PreCallback和PostCallback执行时间为5秒。在开始和结束执行Add操作,以及开始与结束回调的时候都是通过EventMonitor发送相应的事件通知。修改后的CalculatorService如下面的代码所示。

   1: [ServiceBehavior(UseSynchronizationContext = false,InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Reentrant)]
   2: public class CalculatorService : ICalculator
   3: {
   4:     public void Add(double x, double y)
   5:     {
   6:         //PreCallback
   7:         EventMonitor.Send(EventType.StartExecute);
   8:         Thread.Sleep(5000);
   9:         double result = x + y;
  10:  
  11:         //Callback
  12:         EventMonitor.Send(EventType.StartCallback);
  13:         int clientId = OperationContext.Current.IncomingMessageHeaders.GetHeader<int>(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace);
  14:         MessageHeader<int> messageHeader = new MessageHeader<int>(clientId);
  15:         OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader.GetUntypedHeader(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace));
  16:         OperationContext.Current.GetCallbackChannel<ICalculatorCallback>().ShowResult(result);
  17:         EventMonitor.Send(EventType.EndCallback);
  18:  
  19:         //PostCallback
  20:         Thread.Sleep(5000);
  21:         EventMonitor.Send(EventType.EndExecute);
  22:     }
  23: }

对于服务寄宿程序我们不需要做任何修改,但是我们需要采用支持双向通信的绑定类型以实现对回调的支持,在这里我们采用的是NetTcpBinding。为了降低安全协商(Negotiation)代码对时延,我特意将绑定的安全模式设置成None。下面是更新后的服务端配置,客户端需要进行相应的修改。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>
   4:       <bindings>
   5:         <netTcpBinding>
   6:           <binding name="nonSecureBinding">
   7:             <security mode="None" />
   8:           </binding>
   9:         </netTcpBinding>
  10:       </bindings>
  11:         <services>
  12:             <service name="Artech.ConcurrentServiceInvocation.Service.CalculatorService">
  13:                 <endpoint address="net.tcp://127.0.0.1:3721/calculatorservice" binding="netTcpBinding"
  14:                     bindingConfiguration="nonSecureBinding" contract="Artech.ConcurrentServiceInvocation.Service.Interface.ICalculator" />
  15:             </service>
  16:         </services>
  17:     </system.serviceModel>
  18: </configuration>

由于回调操组在客户端执行,所以客户端首先需要的就是实现回调契约接口创建回调类型。实现回调契约接口的ICalculatorCallback定义在CalculatorCallbackService类型中。由于在本例中我们需要的仅仅监控回调操作执行的时间,并不是真的需要显示出运算的最终结果。所以我们仅仅是通过挂起当前线程模拟一个耗时的回调操作(10秒),在回调操作开始和结束执行的时候通过EventMonitor发送相应的事件通知。

   1: using System.ServiceModel;
   2: using System.Threading;
   3: using Artech.ConcurrentServiceInvocation.Service.Interface;
   4: namespace Artech.ConcurrentServiceInvocation.Client
   5: {
   6:     public class CalculatorCallbackService : ICalculatorCallback
   7:     {
   8:         public void ShowResult(double result)
   9:         {
  10:             EventMonitor.Send(EventType.StartExecuteCallback);
  11:             Thread.Sleep(10000);
  12:             EventMonitor.Send(EventType.EndExecuteCallback);
  13:         }
  14:     }
  15: }

最后一个步骤是对客户端按照回调的方式进行相应的修改。首先我们创建CalculatorCallbackService对象,并以此创建一个InstanceContext作为回调实例上下文。然后通过该InstanceContext创建DuplexChannelFactory<TChannel>。最后通过ThreadPool并发地执行2次服务代理的创建和服务调用的操作,客户端ID作为消息报头被传送到服务端。

   1: public partial class MonitorForm : Form
   2: {
   3:     private SynchronizationContext _syncContext;
   4:     private DuplexChannelFactory<ICalculator> _channelFactory;
   5:     private InstanceContext _callbackInstance;
   6: private int _clientId = 0;
   7:  
   8:     //其他成员
   9:     private void MonitorForm_Load(object sender, EventArgs e)
  10:     {
  11:         string header = string.Format("{0, -13}{1, -22}{2}", "Client", "Time", "Event");
  12:         this.listBoxExecutionProgress.Items.Add(header);
  13:         _syncContext = SynchronizationContext.Current;
  14:         _callbackInstance = new InstanceContext(new CalculatorCallbackService());
  15:         _channelFactory = new  DuplexChannelFactory<ICalculator>(_callbackInstance,"calculatorservice");
  16:  
  17:         EventMonitor.MonitoringNotificationSended += ReceiveMonitoringNotification;
  18:         this.Disposed += delegate
  19:         {
  20:             EventMonitor.MonitoringNotificationSended -= ReceiveMonitoringNotification;
  21:             _channelFactory.Close();
  22:         };
  23:  
  24:         for (int i = 0; i < 2; i++)
  25:         {
  26:             ThreadPool.QueueUserWorkItem(state =>
  27:             {
  28:                 int clientId = Interlocked.Increment(ref _clientId);
  29:                 EventMonitor.Send(clientId, EventType.StartCall);
  30:                 ICalculator proxy = _channelFactory.CreateChannel();
  31:                 using (OperationContextScope contextScope = new OperationContextScope(proxy as IContextChannel))
  32:                 {
  33:                     MessageHeader<int> messageHeader = new MessageHeader<int>(clientId);
  34:                     OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader.GetUntypedHeader(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace));
  35:                     proxy.Add(1, 2);
  36:                 }
  37:                 EventMonitor.Send(clientId, EventType.EndCall);
  38:             }, null);
  39:         }
  40:     } 
  41: }

二、从并发控制机制分析得到的输出结果

现在重新运行我们更新后的监控程序,你将会得到如图1所示的输出结果。如果你仔细分析服务端和客户端输出的结果你将会看到Add操作的整个执行时间有一段是重合的,也就是说整个服务操作存在并发执行的情况。但是单看PreCallback和PostCallback,则不存在并发执行的情况。从客户端的角度来看,回调操作也不存在并发执行的情况

image

图1 Reentrant(Service) + Single(Callback)监控结果

可能上面的输出结果还不是很直观,现在我们通过时间轴的形式来描述通过输出结果表现出的执行情况。我们忽略掉客户端和服务通信以及WCF消息分发导致的时延,两次服务调用在执行的情况如图2所示。假设服务端在0s接收到两个并发的调用请求,一个请求被分发给InstanceContext,另一个则被放到等待队列。到5s的时候,第一个请求完成PreCallback的操作后进行回调,此时InstanceContext被释放出来,使得它可以用于处理等待着的第二个请求。到10s的时候,第二个请求完成了PreCallback操作准备进行回调,但是封装回调实例的InstanceContext正在处理第一个回调请求,所示自己在一个等待,直到20s时第一个回调请求处理完毕。

image

图2 Reentrant(Service) + Single(Callback)监控结果时间轴描述

上面我们模拟的时单例实例上下文情况下,服务和回调分别采用Concurrency.Reentrant和Concurrency.Single的情况。实例演示的结果充分证明在《并发中的同步--WCF并发体系的同步机制实现》中关于针对InstanceContext加锁的同步机制的分析。进一步地,如果按照我们的分析,如果我们同时将服务和回调采用的并发模式均换成Concurrency.Multiple,那么无论是作用于服务实例上下文的PreCallback和PostCallback操作,还是作用于回调实例上下文的Callback都可以并发地执行。为此,我们只需要对分别应用于CalculatorService和CalculatorCallbackService的ServiceBehaviorAttribute和CallbackBehaviorAttribute的两个特性稍加修改,将ConcurrencyMode属性设置成ConcurrencyMode.Multiple即可。相应的改动如下面的代码所示:

   1: [ServiceBehavior(UseSynchronizationContext = false,InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
   2: public class CalculatorService : ICalculator
   3: {
   4:    //省略成员
   5: }
   6:  
   7: [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)]
   8: public class CalculatorCallbackService : ICalculatorCallback
   9: {
  10: //省略成员
  11: }

再次运行我们的监控程序,得到的如图3所示的输出,可以看出这正是我们希望的结果,无论作用于那个InstanceContext的操作都是并发执行的

image

图3 Multiple(Service) + Multiple(Callback)监控结果

作者:Artech 
出处:http://artech.cnblogs.com 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
分类: [01] 技术剖析
标签: WCF, Concurrency, Callback, InstanceContext
绿色通道: 好文要顶 关注我 收藏该文与我联系 
Artech
关注 - 53
粉丝 - 3112
荣誉:推荐博客
+加关注
5
0
(请您对文章做出评价)
« 博主前一篇:并发与实例上下文模式: WCF服务在不同实例上下文模式下具有怎样的并发表现
» 博主后一篇:ConcurrencyMode.Multiple模式下的WCF服务就一定是并发执行的吗:探讨同步上下文对并发的影响[上篇]
posted @ 2010-03-31 18:41 Artech 阅读(2966) 评论(19) 编辑 收藏

  
#1楼[楼主2010-03-31 18:45 Artech  
三月份最后一篇博文,呵呵!
支持(0)反对(0)
  
#2楼 2010-03-31 18:53 Robin Zhang  
Artech,你这发文速度都超过我看文了。
太厉害了
支持(0)反对(0)
  
#3楼[楼主2010-03-31 18:56 Artech  
引用Robin Zhang:
Artech,你这发文速度都超过我看文了。
太厉害了

很多是之前就写好的,不然哪里有这么快呢:)
支持(0)反对(0)
  
#4楼 2010-03-31 19:03 GaryChen  
你这个博文的速度真的很快...太暴力了
支持(0)反对(0)
  
#5楼[楼主2010-03-31 19:26 Artech  
引用GaryChen:你这个博文的速度真的很快...太暴力了

呵呵!
支持(0)反对(0)
  
#6楼 2010-03-31 20:23 longgel  
老A太利害了。。出的太快了
支持(0)反对(0)
  
#7楼[楼主2010-03-31 21:32 Artech  
引用longgel:老A太利害了。。出的太快了

并非现写现发,所以比较频繁:)
支持(0)反对(0)
  
#8楼 2010-03-31 23:40 贺爱平  
哥,你的脸真白
支持(0)反对(0)
  
#9楼 2010-04-01 00:36 ξ箫音ξ  
写的不错,如果在加个总结,就更好了。
支持(0)反对(0)
  
#10楼[楼主2010-04-01 07:52 Artech  
引用ξ箫音ξ:写的不错,如果在加个总结,就更好了。

其实,总结点在前几篇文章中。
祝贺高大哥新书出版!
支持(0)反对(0)
  
#11楼[楼主2010-04-01 07:55 Artech  
引用贺爱平:哥,你的脸真白

你应该说“哥,你皮肤真好,白”!
支持(0)反对(0)
  
#12楼 2010-04-01 17:41 贺爱平  
哥,你皮肤真好,白
我嫉妒,我的皮肤是好,但是黑,差距咋就这么大呢
支持(0)反对(0)
  
#13楼 2010-04-07 14:59 近近  
很好啊,很实用,正好能看懂也。
支持(0)反对(0)
  
#14楼[楼主2010-04-07 18:10 Artech  
引用贺爱平:
哥,你皮肤真好,白
我嫉妒,我的皮肤是好,但是黑,差距咋就这么大呢

:)
支持(0)反对(0)
  
#15楼[楼主2010-04-07 18:11 Artech  
引用近近:很好啊,很实用,正好能看懂也。

呵呵,“正好能看懂也”,我觉得我的文章都挺直白的:)
支持(0)反对(0)
  
#16楼 2010-04-09 11:04 denli  
EventMonitor.Send(EventType.StartExecuteCallback);
这句话是什么意思啊?
用途具体是什么呢,在什么情况下使用?
请指教。
支持(0)反对(0)
  
#17楼[楼主2010-04-09 17:02 Artech  
看来你没有仔细看,呵呵!
前面一篇的例子:
http://www.cnblogs.com/artech/archive/2010/03/22/1691862.html
支持(0)反对(0)
  
#18楼 2012-06-04 09:53 dgh  
WCF双通道的模式下,比如启动3个客户端,这3个客户端都实现了服务器回调,他们的启动顺序是1、2、3,你关闭1客户端,那么2、3客户端还能不能回调成功。
在我实现的WCF双通道的模式下,这种情况,都无法回调2和3客户端,不知道什么原因,还请指教,谢谢
支持(0)反对(0)
  
#19楼[楼主2012-06-04 09:57 Artech  
@dgh
引用WCF双通道的模式下,比如启动3个客户端,这3个客户端都实现了服务器回调,他们的启动顺序是1、2、3,你关闭1客户端,那么2、3客户端还能不能回调成功。
在我实现的WCF双通道的模式下,这种情况,都无法回调2和3客户端,不知道什么原因,还请指教,谢谢

可以!


原创粉丝点击