回调与并发: 通过实例剖析WCF基于ConcurrencyMode.Reentrant模式下的并发控制机制
2010-03-31 18:41
543 查看
对于正常的服务调用,从客户端发送到服务端的请求消息最终会被WCF服务运行时分发到相应的封装了服务实例的InstanceContext上。而在回调场景中,我们同样将回调对象封装到InstanceContext对象,并将其封送到客户端。当服务操作过程中执行回调操作的时候,回调消息最终也是分发到位于客户端封装回调对象的InstanceContext。从消息分发与并发处理的机制来看,这两种请求并没有本质的不同。接下来,我们通过《实践重于理论》中的实例,综合分析WCF对并发服务调用和并发回调的处理机制。
作为回调契约的ICalculatorCallback接口定义如下,计算结果传入ShowResult方法显示出来。在一般情况下,我们会将Add和ShowResult和操作定义在单向(One-way),但是这里我并没有这么做,所以无论是服务操作Add还是回调操作ShowResult均采用请求/回复消息交换模式。
在本例中我们的CalculatorService采用单例实例上下文模式(InstanceContextMode.Single)。为了能够执行回调,将并发模式设置成ConcurrencyMode.Reentrant。在Add操作中,我们可以将整个执行过程分成三个阶段:PreCallback、Callback和PostCallback,而且PreCallback和PostCallback执行时间为5秒。在开始和结束执行Add操作,以及开始与结束回调的时候都是通过EventMonitor发送相应的事件通知。修改后的CalculatorService如下面的代码所示。
对于服务寄宿程序我们不需要做任何修改,但是我们需要采用支持双向通信的绑定类型以实现对回调的支持,在这里我们采用的是NetTcpBinding。为了降低安全协商(Negotiation)代码对时延,我特意将绑定的安全模式设置成None。下面是更新后的服务端配置,客户端需要进行相应的修改。
由于回调操组在客户端执行,所以客户端首先需要的就是实现回调契约接口创建回调类型。实现回调契约接口的ICalculatorCallback定义在CalculatorCallbackService类型中。由于在本例中我们需要的仅仅监控回调操作执行的时间,并不是真的需要显示出运算的最终结果。所以我们仅仅是通过挂起当前线程模拟一个耗时的回调操作(10秒),在回调操作开始和结束执行的时候通过EventMonitor发送相应的事件通知。
最后一个步骤是对客户端按照回调的方式进行相应的修改。首先我们创建CalculatorCallbackService对象,并以此创建一个InstanceContext作为回调实例上下文。然后通过该InstanceContext创建DuplexChannelFactory<TChannel>。最后通过ThreadPool并发地执行2次服务代理的创建和服务调用的操作,客户端ID作为消息报头被传送到服务端。
图1 Reentrant(Service) + Single(Callback)监控结果
可能上面的输出结果还不是很直观,现在我们通过时间轴的形式来描述通过输出结果表现出的执行情况。我们忽略掉客户端和服务通信以及WCF消息分发导致的时延,两次服务调用在执行的情况如图2所示。假设服务端在0s接收到两个并发的调用请求,一个请求被分发给InstanceContext,另一个则被放到等待队列。到5s的时候,第一个请求完成PreCallback的操作后进行回调,此时InstanceContext被释放出来,使得它可以用于处理等待着的第二个请求。到10s的时候,第二个请求完成了PreCallback操作准备进行回调,但是封装回调实例的InstanceContext正在处理第一个回调请求,所示自己在一个等待,直到20s时第一个回调请求处理完毕。
图2 Reentrant(Service) + Single(Callback)监控结果时间轴描述
上面我们模拟的时单例实例上下文情况下,服务和回调分别采用Concurrency.Reentrant和Concurrency.Single的情况。实例演示的结果充分证明在《并发中的同步--WCF并发体系的同步机制实现》中关于针对InstanceContext加锁的同步机制的分析。进一步地,如果按照我们的分析,如果我们同时将服务和回调采用的并发模式均换成Concurrency.Multiple,那么无论是作用于服务实例上下文的PreCallback和PostCallback操作,还是作用于回调实例上下文的Callback都可以并发地执行。为此,我们只需要对分别应用于CalculatorService和CalculatorCallbackService的ServiceBehaviorAttribute和CallbackBehaviorAttribute的两个特性稍加修改,将ConcurrencyMode属性设置成ConcurrencyMode.Multiple即可。相应的改动如下面的代码所示:
再次运行我们的监控程序,得到的如图3所示的输出,可以看出这正是我们希望的结果,无论作用于那个InstanceContext的操作都是并发执行的。
图3 Multiple(Service) + Multiple(Callback)监控结果
一、将实例改成支持回调的形式
为此,我们需要对我们上面给出的监控程序进行相应的修改。首先需要修改的是服务契约ICalculator。服务契约ICalculator的Add操作接受传入的操作数并以返回值得形式返回到客户端。现在我们通过回调的形式来重写计算服务:将Add的返回类型改称void,计算结果通过执行回调操作的形式在客户端显示。[ServiceContract(Namespace="http://www.artech.com/",CallbackContract =typeof(ICalculatorCallback))] public interface ICalculator { [OperationContract] void Add(double x, double y); }
作为回调契约的ICalculatorCallback接口定义如下,计算结果传入ShowResult方法显示出来。在一般情况下,我们会将Add和ShowResult和操作定义在单向(One-way),但是这里我并没有这么做,所以无论是服务操作Add还是回调操作ShowResult均采用请求/回复消息交换模式。
using System.ServiceModel; namespace Artech.ConcurrentServiceInvocation.Service.Interface { [ServiceContract(Namespace = "http://www.artech.com/")] public interface ICalculatorCallback { [OperationContract] void ShowResult(double result); } }
在本例中我们的CalculatorService采用单例实例上下文模式(InstanceContextMode.Single)。为了能够执行回调,将并发模式设置成ConcurrencyMode.Reentrant。在Add操作中,我们可以将整个执行过程分成三个阶段:PreCallback、Callback和PostCallback,而且PreCallback和PostCallback执行时间为5秒。在开始和结束执行Add操作,以及开始与结束回调的时候都是通过EventMonitor发送相应的事件通知。修改后的CalculatorService如下面的代码所示。
[ServiceBehavior(UseSynchronizationContext = false,InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Reentrant)] public class CalculatorService : ICalculator { public void Add(double x, double y) { //PreCallback EventMonitor.Send(EventType.StartExecute); Thread.Sleep(5000); double result = x + y; //Callback EventMonitor.Send(EventType.StartCallback); int clientId = OperationContext.Current.IncomingMessageHeaders.GetHeader<int>(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace); MessageHeader<int> messageHeader = new MessageHeader<int>(clientId); OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader.GetUntypedHeader(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace)); OperationContext.Current.GetCallbackChannel<ICalculatorCallback>().ShowResult(result); EventMonitor.Send(EventType.EndCallback); //PostCallback Thread.Sleep(5000); EventMonitor.Send(EventType.EndExecute); } }
对于服务寄宿程序我们不需要做任何修改,但是我们需要采用支持双向通信的绑定类型以实现对回调的支持,在这里我们采用的是NetTcpBinding。为了降低安全协商(Negotiation)代码对时延,我特意将绑定的安全模式设置成None。下面是更新后的服务端配置,客户端需要进行相应的修改。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <bindings> <netTcpBinding> <binding name="nonSecureBinding"> <security mode="None" /> </binding> </netTcpBinding> </bindings> <services> <service name="Artech.ConcurrentServiceInvocation.Service.CalculatorService"> <endpoint address="net.tcp://127.0.0.1:3721/calculatorservice" binding="netTcpBinding" bindingConfiguration="nonSecureBinding" contract="Artech.ConcurrentServiceInvocation.Service.Interface.ICalculator" /> </service> </services> </system.serviceModel> </configuration>
由于回调操组在客户端执行,所以客户端首先需要的就是实现回调契约接口创建回调类型。实现回调契约接口的ICalculatorCallback定义在CalculatorCallbackService类型中。由于在本例中我们需要的仅仅监控回调操作执行的时间,并不是真的需要显示出运算的最终结果。所以我们仅仅是通过挂起当前线程模拟一个耗时的回调操作(10秒),在回调操作开始和结束执行的时候通过EventMonitor发送相应的事件通知。
using System.ServiceModel; using System.Threading; using Artech.ConcurrentServiceInvocation.Service.Interface; namespace Artech.ConcurrentServiceInvocation.Client { public class CalculatorCallbackService : ICalculatorCallback { public void ShowResult(double result) { EventMonitor.Send(EventType.StartExecuteCallback); Thread.Sleep(10000); EventMonitor.Send(EventType.EndExecuteCallback); } } }
最后一个步骤是对客户端按照回调的方式进行相应的修改。首先我们创建CalculatorCallbackService对象,并以此创建一个InstanceContext作为回调实例上下文。然后通过该InstanceContext创建DuplexChannelFactory<TChannel>。最后通过ThreadPool并发地执行2次服务代理的创建和服务调用的操作,客户端ID作为消息报头被传送到服务端。
[code] public partial class MonitorForm : Form { private SynchronizationContext _syncContext; private DuplexChannelFactory<ICalculator> _channelFactory; private InstanceContext _callbackInstance; private int _clientId = 0; //其他成员 private void MonitorForm_Load(object sender, EventArgs e) { string header = string.Format("{0, -13}{1, -22}{2}", "Client", "Time", "Event"); this.listBoxExecutionProgress.Items.Add(header); _syncContext = SynchronizationContext.Current; _callbackInstance = new InstanceContext(new CalculatorCallbackService()); _channelFactory = new DuplexChannelFactory<ICalculator>(_callbackInstance,"calculatorservice"); EventMonitor.MonitoringNotificationSended += ReceiveMonitoringNotification; this.Disposed += delegate { EventMonitor.MonitoringNotificationSended -= ReceiveMonitoringNotification; _channelFactory.Close(); }; for (int i = 0; i < 2; i++) { ThreadPool.QueueUserWorkItem(state => { int clientId = Interlocked.Increment(ref _clientId); EventMonitor.Send(clientId, EventType.StartCall); ICalculator proxy = _channelFactory.CreateChannel(); using (OperationContextScope contextScope = new OperationContextScope(proxy as IContextChannel)) { MessageHeader<int> messageHeader = new MessageHeader<int>(clientId); OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader.GetUntypedHeader(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace)); proxy.Add(1, 2); } EventMonitor.Send(clientId, EventType.EndCall); }, null); } } }[/code]
二、从并发控制机制分析得到的输出结果
现在重新运行我们更新后的监控程序,你将会得到如图1所示的输出结果。如果你仔细分析服务端和客户端输出的结果你将会看到Add操作的整个执行时间有一段是重合的,也就是说整个服务操作存在并发执行的情况。但是单看PreCallback和PostCallback,则不存在并发执行的情况。从客户端的角度来看,回调操作也不存在并发执行的情况。图1 Reentrant(Service) + Single(Callback)监控结果
可能上面的输出结果还不是很直观,现在我们通过时间轴的形式来描述通过输出结果表现出的执行情况。我们忽略掉客户端和服务通信以及WCF消息分发导致的时延,两次服务调用在执行的情况如图2所示。假设服务端在0s接收到两个并发的调用请求,一个请求被分发给InstanceContext,另一个则被放到等待队列。到5s的时候,第一个请求完成PreCallback的操作后进行回调,此时InstanceContext被释放出来,使得它可以用于处理等待着的第二个请求。到10s的时候,第二个请求完成了PreCallback操作准备进行回调,但是封装回调实例的InstanceContext正在处理第一个回调请求,所示自己在一个等待,直到20s时第一个回调请求处理完毕。
图2 Reentrant(Service) + Single(Callback)监控结果时间轴描述
上面我们模拟的时单例实例上下文情况下,服务和回调分别采用Concurrency.Reentrant和Concurrency.Single的情况。实例演示的结果充分证明在《并发中的同步--WCF并发体系的同步机制实现》中关于针对InstanceContext加锁的同步机制的分析。进一步地,如果按照我们的分析,如果我们同时将服务和回调采用的并发模式均换成Concurrency.Multiple,那么无论是作用于服务实例上下文的PreCallback和PostCallback操作,还是作用于回调实例上下文的Callback都可以并发地执行。为此,我们只需要对分别应用于CalculatorService和CalculatorCallbackService的ServiceBehaviorAttribute和CallbackBehaviorAttribute的两个特性稍加修改,将ConcurrencyMode属性设置成ConcurrencyMode.Multiple即可。相应的改动如下面的代码所示:
[ServiceBehavior(UseSynchronizationContext = false,InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)] public class CalculatorService : ICalculator { //省略成员 } [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)] public class CalculatorCallbackService : ICalculatorCallback { //省略成员 }
再次运行我们的监控程序,得到的如图3所示的输出,可以看出这正是我们希望的结果,无论作用于那个InstanceContext的操作都是并发执行的。
图3 Multiple(Service) + Multiple(Callback)监控结果
相关文章推荐
- 回调与并发: 通过实例剖析WCF基于ConcurrencyMode.Reentrant模式下的并发控制机制
- 通过意向锁多粒度封锁机制进行并发控制
- 在C++中使用事件回调机制(通过Observer模式、函数指针等实现)
- WCF并发控制与实例模式
- Java进阶——回调机制详解及实例
- Linux内核开发之并发控制(实例总结篇)
- WCF技术剖析之二十三:服务实例(Service Instance)生命周期如何控制[中篇]
- WCF技术剖析之二十三:服务实例(Service Instance)生命周期如何控制[下篇]
- boost源码剖析之:多重回调机制signal(上)
- 改进的单例模式,解决并发生成多实例问题,并且提高调用时的性能
- Android应用中通过AIDL机制实现进程间的通讯实例
- boost源码剖析之:多重回调机制signal(下)
- boost源码剖析之:多重回调机制signal(上)
- 通过js控制时间,一秒一秒自己动的实例
- 并发与实例模式
- 剖析Node.js异步编程中的回调与代码设计模式
- Java并发控制机制详解
- 通过实例浅谈Spring运作机制
- 通过实例感受设计模式之策略模式