在《上篇》中,我通过一个具体的实例演示了WCF服务宿主的同步上下文对并发的影响,并简单地介绍了同步上下文是什么东东,以及同步上下文在多线程中的应用。那么,同步上下文在WCF并发体系的内部是如何影响服务操作的执行的呢?这实际上涉及到WCF的一个话题,即线程的亲和性(Thread Affinity),本篇文章将为你剖析WCF线程亲和机制的本质。
一、WCF线程亲和性(Thread Affinity)
对于服务端来说,WCF消息监听和接收体系通过IO线程池并发的处理来自客户端的服务调用请求,所以并发抵达的服务调用请求消息能够得到及时的处理。但是,服务操作具体在那个线程线程执行,则是通过WCF的并发处理体系决定的。
在默认的情况下,WCF采用这样的机制控制并发操作的执行:如果在进行服务寄宿(IIS寄宿方式除外)的过程中,当前线程存在同步上下文,会将其保存在服务端分发运行时。等到需要执行服务操作的时候,WCF并发体系会判断分发运行时的同步上下文是否存在,如果不存在则在各个的线程中执行服务操作,否则,服务操作会被封送到该同步上下文中执行。
如果我们将某个服务寄宿于一个控制台(Console)应用之中,由于控制台程序的当前同步上下文为空,按照上面的并发操作执行机制,所有的请求操作会在各自的线程中并行地执行。所以,在流量允许的范围内,并发的请求能够得到及时地处理。
如果我们通过Windows Forms应用作为某个服务的宿主,服务操作的执行永远是以同步的方式执行的。也就是说,在某个时刻,仅仅只有针对某个服务调用的服务操作被执行,其他的调用请求都将被放入一个等待队列中。如果等待的时候超出了设定的超时时限(这在高并发的情况下会比较频繁),客户端会抛出TimeoutException异常。我们将服务操作与服务寄宿程序线程自动绑定的现象称为服务的线程亲和性(Thread Affinity)
在这种情况下,由于服务操作执行才UI线程中,可以正常得对Windows窗体上的控件进行操作。如果,你希望服务操作能够并发地被执行,就不得不打破这种线程亲和性,我们可以按照监控程序中的方式在服务类型上应用ServiceBehaviorAttribute特性将UseSynchronizationContext属性设置成False。但是在这种情况下,如果你需要对控件进行操作,你就需要调用Control类型的Invoke或者BeginInvoke方法,或者按照我们监控程序的方式借助于SynchronizationContext对象了。UseSynchronizationContext在ServiceBehaviorAttribute的定义如下面的代码所示,该属性的默认值为True。
代码语言:js复制 1: [AttributeUsage(AttributeTargets.Class)]
2: public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
3: {
4: //其他成员
5: public bool UseSynchronizationContext { get; set; }
6: }
通过上面的介绍,我想读者对WCF的并发请求操作的执行机制有了一个大概的了解,接下来我们对该机制在WCF并发框架体系下的真正实现进行更加深层次的探讨。
二、同步上下文如何影响服务操作的执行?
当服务端信道层成功介绍到来其客户端的请求消息后,会将该消息递交给相应信道监听器(Channel Listener)所在的信道分发器(Channel Dispatcher)。信道分发器则根据相应的消息筛选(Message Filter)将消息进一步分发给匹配的终结点分发器(Endpoint Dispatcher)。终结点分发器根据自己的分发运行时(Dispatch Runtime)设定的处理行为对请求消息执行进一步的处理。关于消息分发、筛选机制,以及分发运行时的创建,在《WCF技术剖析(卷1)》的第2章和第7章有详细的介绍。
分发运行时控制了终结点分发器进行消息处理的行为,实际上我们大部分作用于服务端自定义行为(契约行为、操作行为、服务行为和终结点行为)都是通过对该运行时进行相应的定制,使得WCF服务端框架按照我们希望的方式处理请求的消息。分发运行时通过System.ServiceModel.Dispatcher.DispatchRuntime类型表示,在DispatchRuntime中定义了如下一个SynchronizationContext属性。
代码语言:js复制 1: public sealed class DispatchRuntime
2: {
3: //其他成员
4: public SynchronizationContext SynchronizationContext { get; set; }
5: }
如果读者对《WCF技术剖析(卷1)》第7章关于服务寄宿的整个流程不那么陌生的话,应该知道DispatchRuntime是在服务寄宿过程中被初始化的。在DispatchRuntime初始化过程中,会按照如下所示的伪代码对SynchronizationContext进行初始化。初始化逻辑比较简单,首先会遍历当前AppDomain中是否加载了名称以“System.Web,”为前缀的程序集,如果这样的程序集被成功加载,并且HostingEnvironment的IsHosted返回值为True,则将SynchronizationContext设置为Null。该步骤主要是判断服务寄宿的方式是否为IIS,因为这样的寄宿方式不需要同步上下文。实际上,如果你采用ASP.NET应用作为宿主,下面的代码也是进行与IIS寄宿一样的逻辑分支。如果不满足上面的条件,则将当前线程的同步上下文赋值给SynchronizationContext属性。所以,对于Windows Forms应用作为服务的宿主,DispatchRuntime的SynchronizationContext将会被初始化成一个WindowsFormsSynchronizationContext对象。
代码语言:js复制 1: public sealed class DispatchRuntime
2: {
3: //其他成员
4: public SynchronizationContext SynchronizationContext { get; set; }
5: public DispatchRuntime()
6: {
7: //其他操作
8: Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
9: for (int i = 0; i < assemblies.Length; i )
10: {
11: if (string.Compare(assemblies[i].FullName, 0, "System.Web,", 0, "System.Web,".Length, StringComparison.OrdinalIgnoreCase) == 0)
12: {
13: if (HostingEnvironment.IsHosted)
14: {
15: this.SynchronizationContext = null;
16: return;
17: }
18: }
19: }
20: this.SynchronizationContext = SynchronizationContext.Current;
21: }
22: }
如果对《WCF技术剖析(卷1)》第9章介绍的关于服务整个过程的剖析还不曾遗忘的话,应该知道在初始化DispatchRuntime之后,开启ServiceHost之后,还有一项重要的操作就是应用所有相关的分发行为。具体来讲,就是遍历所有相关的契约行为、操作行为、服务行为和终结点行为,调用ApplyDispatchBehavior对象。那么,这肯定涉及到对ServiceBehaviorAttribute的ApplyDispatchBehavior方法的调用。
在ServiceBehaviorAttribute的ApplyDispatchBehavior方法中,会根据UseSynchronizationContext属性值对DispatchRuntime的SynchronizationContext属性进行重新设定。具体来讲,如果UseSynchronizationContext属性为False,会将SynchronizationContext设置为NULL。相应的逻辑可以通过下面的伪代码表示:
代码语言:js复制 1: public class ServiceBehaviorAttribute : Attribute, IServiceBehavior
2: {
3: //其他成员
4: public bool UseSynchronizationContext { get; set; }
5: public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
6: {
7: //其他操作
8: if (!UseSynchronizationContext)
9: {
10: foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
11: {
12: foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
13: {
14: endpointDispatcher.DispatchRuntime.SynchronizationContext = null;
15: }
16: }
17: }
18: }
19: }
当真正的服务调用请求被分发给终结点分发器后,会先判断DispatchRuntime的SynchronizationContext是否存在。如果返回为NULL,请求消息会在各自的线程中进行处理,否则,会将后续的消息处理操作奉送到该SynchronizationContext表示的同步上下文中执行。因此,在DispatchRuntime的SynchronizationContext存在的情况下,后续的消息处理过程都是以同步的方式执行的。终结点分发器对请求消息的处理大体上可以通过下面一段伪代码表示:
代码语言:js复制 1: public void ProcessMessage(Message message)
2: {
3: SendOrPostCallback messageProcessCallback = GetMessageProcessCallback();
4: DispatchRuntime dispatchRuntime = GetCurrentRuntime();
5: SynchronizationContext context = dispatchRuntime.SynchronizationContext;
6: if (context != null)
7: {
8: context.Post(messageProcessCallback, message);
9: }
10: else
11: {
12: IOThreadScheduler.ScheduleCallback(messageProcessCallback, message);
13: }
14: }
三、 同步上下文如何影响回调操作的执行?
上面我们谈到WCF服务端并发体系基于同步上下文的处理机制,从中我们知道了对于非IIS和ASP.NET的寄宿方式,如果在进行服务寄宿的时候当前线程存在同步上下文(比如Windows Forms应用作为宿主),服务操作最终是在该同步上下文中执行的。
相似的情况向同样发生在回调操作的执行上面。在回调场景中,客户端开启服务代理并指定回调实例上下文对象进行服务调用的时候,如果当前线程存在同步上下文,那么当服务端进行回调的时候,回调操作会自动被封送到该同步上下文中执行。也就是说,回调操作与客户端程序也存在一种线程关联性。
在服务端,我们可以通过在服务类型上面应用ServiceBehaviorAttribute特性并将UseSynchronizationContext属性设置成False,来解除服务操作与服务寄宿程序之间的线程关联性。在客户端,我们也可以采用特性标注的方式解除掉回调操作与客户端程序之间的线程关联性,而这个特性就是我们之前提到过的CallbackBehaviorAttribute。如下面的代码所示,CallbackBehaviorAttribute特性同样具有一个UseSynchronizationContext属性。
代码语言:js复制 1: [AttributeUsage(AttributeTargets.Class)]
2: public sealed class CallbackBehaviorAttribute : Attribute, IEndpointBehavior
3: {
4: //其他成员
5: public bool UseSynchronizationContext { get; set; }
6: }
如下面的代码所示,我们只需要在回调类型上应用CallbackBehaviorAttribute特性,并将UseSynchronizationContext设置成False,就可以解除回调操作与客户端程序之间的线程关联性。在这种情况下,回调操作将会在接受回调请求的IO线程中执行。
代码语言:js复制 1: [CallbackBehavior(UseSynchronizationContext = false)]
2: public class CalculatorCallbackService : ICalculatorCallback
3: {
4: //省略成员
5: }