由于WCF的并发是针对某个封装了服务实例的InstanceContext而言的,所以在不同的实例上下文模式下,会表现出不同的并发行为。接下来,我们从具体的实例上下文模式的角度来剖析WCF的并发,如果对WCF实例上下文模式和实例上下文提供机制不了解的话,请参阅《WCF技术剖析(卷1)》第9章。
在《实践重于理论》一文中,我写一个了简单的WCF应用,通过这个应用我们可以很清楚了监控客户端和服务操作的执行情况下。借此,我们可以和直观地看到服务端对于并发的服务调用请求,到底采用的是并行还是串行的执行方式。接下来,我们将充分地利用这个监控程序,以实例演示加原理分析相结合的方式对不同实例上下文模式下的并发实现机制进行深度剖析。
由于WCF的并发是针对某个封装了服务实例的InstanceContext而言的,但是对单调的实例上下文模式,WCF服务端运行时总是创建一个全新的InstanceContext来处理每一个请求,不管该请求是否来自相同的客户端。所以在单调实例上下文模式下,根本就不存在对某个InstanceContext的并发调用的情况发生。
我们可以通过我们监控程序来验证这一点。为此,我们需要通过ServiceBehaviorAttribute将实例上下文模式设置成InstanceContextMode.PerCall,相关的代码如下所示。
1: [ServiceBehavior(UseSynchronizationContext = false,InstanceContextMode = InstanceContextMode.PerCall)]
2: public class CalculatorService : ICalculator
3: {
4: //省略成员
5: }
下面是客户端进行并发服务调用的代码:
1: for (int i = 1; i <= 5; i++)
2: {
3: ThreadPool.QueueUserWorkItem(state =>
4: {
5: int clientId = Interlocked.Increment(ref clientIdIndex);
6: ICalculator proxy = _channelFactory.CreateChannel();
7: using (proxy as IDisposable)
8: {
9: EventMonitor.Send(clientId, EventType.StartCall);
10: using (OperationContextScope contextScope = new OperationContextScope(proxy as IContextChannel))
11: {
12: MessageHeader<int> messageHeader = new MessageHeader<int>(clientId);
13: OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader.GetUntypedHeader(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace));
14: proxy.Add(1, 2);
15: }
16: EventMonitor.Send(clientId, EventType.EndCall);
17: }
18: }, null);
19: }
如果在此基础上运行我们的监控程序,将会得到如图1所示的输出结果,从中我们可以看出,仍然我们采用默认的并发模式(ConcurrencyMode.Single),来自5个不同客户端(服务代理)的调用请求能够及时地得到处理。
图1 单调实例上下文模式下的并发事件监控输出(不同客户端)
上面我们演示了WCF服务端处理来自不同客户端并发请求的处理,如果5个请求来自相同的客户端,它们是否还能够及时地得到处理呢?我们不妨通过我们的监控程序来说话。现在我们需要作的是修改客户端进行服务调用的方式,让5个并发的调用来自于相同的服务代理对象,相关的代码如下所示。为了便于跟踪,我们依然将并发的序号1~5通过消息报头传递到服务端。不过在这里它不代表客户端,而是代表某个服务调用而已。
1: ICalculator proxy = _channelFactory.CreateChannel();
2: for (int i = 1; i < 6; i++)
3: {
4: ThreadPool.QueueUserWorkItem(state =>
5: {
6: int clientId = Interlocked.Increment(ref clientIdIndex);
7: EventMonitor.Send(clientId, EventType.StartCall);
8: using (OperationContextScope contextScope = new OperationContextScope(proxy as IContextChannel))
9: {
10: MessageHeader<int> messageHeader = new MessageHeader<int>(clientId);
11: OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader.GetUntypedHeader(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace));
12: proxy.Add(1, 2);
13: }
14: EventMonitor.Send(clientId, EventType.EndCall);
15: }, null);
16: }
再次运行我们的监控程序,你将会得到完全不一样的输出结果(如图2所示)。从监控信息我们可以很清晰地看出,服务操作的执行完全是以串行化的形式执行的。对于服务端来说,似乎仍然是以同步的方式方式处理并发的服务调用请求的。但是我们说过,WCF并发机制的同步机制是通过对InstanceContext进行加锁实现的。但是对于单调实例上下文模式来说,虽然5个请求来自相同的客户端,但是对应的InstanceContext却是不同的。难道我们前面的结论都是错误的吗?
图2 单调实例上下文模式下的并发事件监控输出(相同客户端)
实际上出现如图2所示的监控输出与WCF并发框架体系采用的同步机制一点关系都没有。在说明原因之前,我们先来给出解决方案。我们只需要在进行服务调用之前,调用Open方法显式地开启服务代理,你就会得到与图4-5类似的输出结果,相应的代码如下所示:
1: ICalculator proxy = _channelFactory.CreateChannel();
2: (proxy as ICommunicationObject).Open();
3: for (int i = 1; i < 6; i++)
4: {
5: //省略其他代码
6: }
上面的问题涉及到WCF一个很隐晦的机制,相信不会有太多人知道它的存在。如果我们直接通过创建出来的服务代理对象(并没有显示开启服务代理)进行服务调用,WCF客户端框架会通过相应的机制确保服务代理的开启,我们可以将这种机制成为服务代理自动开启。在内部,WCF实际上是将本次调用放入一个队列之中,等待上一个放入队列的调用结束。也就是说,针对一个没有被显式开启的服务代理的并发调用实际上是以同步或者串行的方式执行的。
但是,如果你在进行服务调用之前通过我们上面代码的方式显式地开启服务代理,基于该代理的服务调用就能得到机制处理。所以,当你真的需要执行基于相同服务代理的并发调用的时候,请务必对服务代理进行显式开启。
并发的问题挺多,到这里还没完。现在我们保留上面修改过的代码(确保在进行并发服务调用之前显示开启服务代理),将客户端和服务终结点采用的绑定类型从WS2007HttpBinding换成NetTcpBinding或者NetNamedPipeBinding。在此运行我们的监控程序,你又将得到类似于如图2所示的监控信息。也就是说,如果采用向NetTcpBinding或者NetNamedPipeBinding这种天生就支持会话的绑定类型(因为它们基于的传输协议提供了对会话的原生支持,HTTP协议本身是没有会话的概念的),对于基于单个服务代理的同步调用,最终表现出来仍就是串行化执行。这是WCF信道架构体系设计使然,我个人对这个设计不以为然。
在基于会话的实例上下文提供机制下,被创建出来封装服务实例的InstanceContext与会话(客户端或者服务代理)绑定在一起。也就是说,InstanceContext和服务代理是具有一一对应的关系。基于我们前面介绍的基于对InstanceContext加锁的同步机制,如果服务端接收到的并发调用是基于不同的客户端,那么它们会被分发给不同的InstanceContext,所以对于它们的处理是并行的。因此,我们主要探讨的是针对相同客户端的并发调用的问题。
在《WCF技术剖析(卷1)》的第9章中,我们对WCF的会话进行过深入的剖析。如果读者对其中的内容还熟悉的话,一定知道WCF的会话最终取决于以下三个方面的因素:
所以说,即使我们通过ServiceBehaviorAttribute特性将服务的实例上下文模式设置成InstanceContextMode.PerSession,如果不满足其余两个条件,WCF仍然采用的是基于单调的实例上下文提供机制,那么表现出来的并发处理行为就与单调模式别无二致了。
我们依然可以通过我们的监控程序来证实这一点,现在我们在CalculatorService类型上应用ServiceBehaviorAttribute特性将实例上下文模式设置成InstanceContextMode.PerSession。
1: [ServiceBehavior(UseSynchronizationContext = false,InstanceContextMode = InstanceContextMode.PerSession)]
2: public class CalculatorService : ICalculator
3: {
4: //省略成员
5: }
然后我们破坏第一个条件,通过ServiceContractAttribute特性将服务契约ICalculator的会话模式设置成SessionMode.NotAllowed。
1: [ServiceContract(Namespace="http://www.artech.com/",SessionMode = SessionMode.NotAllowed)]
2: public interface ICalculator
3: {
4: //省略成员
5: }
我们也可以破环第三个条件,让终结点绑定不支持会话。无论对WSHttpBinding还是WS2007HttpBinding,只有在支持某种安全模式或者可靠会话(Reliable Sessions)的情况下,它们才提供对会话的支持。由于WS2007HttpBinding默认采用基于消息的安全模式,如果我们将安全模式设置成None,绑定将不再支持会话。为此,我们对服务端的配置进行了如下的修改,当然客户端必须进行相应地修改,在这里就不再重复介绍了。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <bindings>
5: <ws2007HttpBinding>
6: <binding name="nonSessionBinding">
7: <security mode="None"/>
8: </binding>
9: </ws2007HttpBinding>
10: </bindings>
11: <services>
12: <service name="Artech.ConcurrentServiceInvocation.Service.CalculatorService">
13: <endpoint bindingConfiguration="nonSessionBinding" address="http://127.0.0.1:3721/calculatorservice" binding="ws2007HttpBinding" contract="Artech.ConcurrentServiceInvocation.Service.Interface.ICalculator" />
14: </service>
15: </services>
16: </system.serviceModel>
17: </configuration>
当我们进行了如此修改后再次运行我们的监控程序,你可以得到类似于如图1所示的表现为并行化处理的监控结果。
如果同时满足上述的三个条件,来自于相同客户端的并发请求是分发到相同的InstanceContext。在这种情况下,WCF将按照相应并发模式语义上体现的行为来处理这些并发的请求。ConcurrencyMode.Single和ConcurrencyMode.Multiple体现的分别是串行化和并行化的处理方式,如果ConcurrencyMode.Reentrant,则后续的请求只有在前一个请求处理结束或者对外调用(Call Out)的时候才有机会被处理。
对于采用单例实例上下文模式,所有的服务调用请求,不论它来自于那个客户端,最终都会被分发给同一个InstanceContext。毫无疑问,在这种情况下最终表现出来的并发处理行为与会话类似。之所以只说类似,是因为单例模式下并没有要求并发请求必须来自相同客户端的限制。