在《并发与实例上下文模式》中,我们通过实例演示的方式讲述了基于不同实例上下文模式的并发行为。对于这个实例中的服务类型CalculatorService,读者应该还记得我们对它进行了特别的定义:通过ServiceBehaviorAttribute特性将属性将UseSynchronizationContext设置成False。至于为何要这么做,这就是本篇文章需要为你讲述的内容。为了让读者对本节介绍的内容有一个深刻的认识,我们不然去掉ServiceBehaviorAttribute特性的UseSynchronizationContext,看看最终会表现出怎样的并发行为。
一、倘若去除ServiceBehaviorAttribute的UseSynchronizationContext属性
现在,我们对监控程序实例中的CalculatorService进行了一些小小的改动,将ServiceBehaviorAttribute特性的UseSynchronizationContext属性设置为True(由于True是默认值,你也可以直接将该属性去掉)。修改后的代码如下所示,采用单调实例上下文模式。你可以通过这里下载整个例子的源代码。
1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, UseSynchronizationContext = true)]
2: public class CalculatorService : ICalculator
3: {
4: //省略成员
5: }
为了让读者得到更多关于并发处理的信息,我们让最终输出的监控信息包含当前线程的ID。为此,我们需要在事件参数类型MonitorEventArgs添加如下一个ThreadId属性。在构造MonitorEventArgs对象的时候,该属性取当前线程ID。
1: public class MonitorEventArgs : EventArgs
2: {
3: //其他成员
4: public int ThreadId
5: { get; private set; }
6:
7: public MonitorEventArgs(int clientId, EventType eventType, DateTime eventTime)
8: {
9: //其他属性赋值
10: this.ThreadId = Thread.CurrentThread.ManagedThreadId;
11: }
12: }
然后我们在寄宿服务的监控窗口中,通过修改用于接收监控信息的方法ReceiveMonitoringNotification,将当前事件对应的线程ID输出来,相应的改动如下所示:
1: public partial class MonitorForm : Form
2: {
3: //其他成员
4: private void MonitorForm_Load(object sender, EventArgs e)
5: {
6: string header = string.Format("{0, -13}{1, -22}{2,-20}{3,-20}", "Client", "Time", "Thread","Event");
7: this.listBoxExecutionProgress.Items.Add(header);
8: //其他操作
9: }
10:
11: public void ReceiveMonitoringNotification(object sender, MonitorEventArgs args)
12: {
13: string message = string.Format({0, -13}{1, -22}{2,-20}{3,-20}", args.ClientId, args.EventTime.ToLongTimeString(), args.ThreadId, args.EventType);
14: _syncContext.Post(state => this.listBoxExecutionProgress.Items.Add(message), null);
15: }
16: }
如果现在运行我们的监控信息,你将会得到如图1所示的输出结果。该监控结果反映了两个重要的信息:服务操作的执行是串行化执行的;服务端采用同一个线程执行的(线程ID相同)。
图1 去除UseSynchronizationContext得到的监控结果
实际上,正是因为所有服务操作的执行都是在同一个线程执行,才会表现出串行化执行的行为。那么,是什么导致客户端并发服务请求最终被分发到同一个线程上面呢?通过上面的分析,我们知道这不可能是WCF并发体系的同步机制所致,因为该不同机制是通过对InstanceContext的锁定来实现的。由于CalculatorService采用的是单调实例上下文模式,每一个服务调用请求都会分发给一个全新的封装有服务实例的InstanceContext。
通过实例我们很清楚地看到,通过去除ServiceBehaviorAttribute特性的UseSynchronizationContext属性定义让我们的服务端失去了并发执行的能力。接下来,我们将着力剖析其背后的原因。不过在这之前,我们需要了解一下UseSynchronizationContext属性中设置到的SynchronizationContext,即同步上下文是什么。
二、 什么是同步上下文(SynchronizationContext)
在一个多线程的应用中,我们经常会遇到这样的场景:在一个异步执行的方法中,需要将部分操作递交给其他某个线程执行。最为典型的场景就是在一个基于Windows Form的GUI应用中,如果异步方法调用涉及到对某个窗体中的某个控件的操作,需要将该操作递交给UI线程中执行,因为控件只能在自己被创建的线程中被操作。这个时候,我们可以采用两种解决方案,其一就是调用System.Windows.Forms.Control的Invoke或者BeginInvoke方法,将相应的操作通过委托的方式传入该方法中执行,其二就是利用同步上下文(SynchronizationContext)。如果细心的朋友,应该已经注意到了在我们前面(《实践重于理论》、《并发与实例上下文模式》和《回调与并发》)广泛使用到的监控程序中,不论在客户端还是服务端,我们写入事件监控信息时就使用到了SynchronizationContext对象。
同步上下文实际上为我们定义这样的编程模式:将某个操作封送(Marshal)到某个指定的线程,使其在目标线程上下文中被执行。同步上下文是在.NET Framework 2.0中被引入一种多线程机制,通过System.Threading.SynchronizationContext表示。SynchronizationContext是一个抽象类,其本身并不提供具体的操作封送的实现。SynchronizationContext定义如下:
1: public class SynchronizationContext
2: {
3: //其他成员
4: public virtual void Post(SendOrPostCallback d, object state);
5: public virtual void Send(SendOrPostCallback d, object state);
6: public static SynchronizationContext Current { get; }
7: }
SynchronizationContext与某个线程绑定,属于线程执行上下文(Execution Context)的一部分,存储于线程本地存储(TLS: Thread Local Storage)中。在SynchronizationContext所有成员中,最重要的就是Send和Post两个方法。调用者调用Send或者Post方法,以SendOrPostCallback委托的形式将相应的操作封送到SynchronizationContext对应的线程中执行。Send和Post具有相同的方法签名,它们之间的不同之处在于Send是基于同步调用,而Post则是异步的。静态只读属性Current获取存贮与当前TLS的SynchronizationContext,如果不存在则返回NULL。
再次回到我们前面闯将的监控程序的例子,对于服务端来说,接收监控事件通知操作和服务操作执行在相同的线程中。如果将ServiceBehaviorAttribute的UseSynchronizationContext属性设置成False,那么该线程就不是服务寄宿的UI线程。对于客户端来说,由于服务调用是以异步的方式进行的,所以接收监控事件通知操作也在UI线程上执行。在输出监控信息的时候,我们需要对监控窗体的空间进行操作,由于控件是在UI线程上被创建的,所以不能在监控线程中对其进行直接操作。异步线程对UI线程的操作,我们就是通过获取UI线程的SynchronizationContext实现的。对于Windows Forms应用,具体的SynchronizationContext类型是System.Windows.Forms.WindowsFormsSynchronizationContext。关于WindowsFormsSynchronizationContext以及SynchronizationContext的其他相关成员的介绍,有兴趣的读者可以参阅MSDN。
为了让读者更加容易地理解SynchronizationContext在WCF并发处理体系中的影响,我们来可以做一个相关的演示实例。我们创建一个Windows Forms应用,添加一个类似于我们监控程序中的窗体,里面仅仅包含用于输出进度信息的ListBox。然后我们在窗体的Load事件中编写如下的代码。
1: int index = 0;
2: SynchronizationContext syncContext = SynchronizationContext.Current;
3: this.listBoxExecutionProgress.Items.Add(string.Format("{0, -10}{1,-10}{2}", "Task", "Thread","Time"));
4: for(int i=0; i<5;i++)
5: {
6: int taskSequence = Interlocked.Increment(ref index);
7: ThreadPool.QueueUserWorkItem(state1 =>
8: {
9: syncContext.Post(state2 =>
10: {
11: Thread.Sleep(5000);
12: string message = string.Format("{0, -10}{1,-10}{2}", taskSequence,Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToLongTimeString());
13: this.listBoxExecutionProgress.Items.Add(message);
14: }, null);
15: }, null);
16: }
上面一段简单的程序模拟这样的场景:通过ThreadPool并行5个相对耗时的操作(每一个耗时5秒,通过让线程休眠实现),并在操作执行结束后打印出当前时间和线程ID。但是,这5个并行操作最终却是在UI线程的SynchronizationContext中执行的。程序运行后将会得到如图2所示的输出结果。
图2 并行操作在相同SynchronizationContext中执行结果
图2反映出来的结果与上面我们去除掉应用在CalculatorServiceAttribute的UseSynchronizationContext属性定义后服务端得到的监控结果比较类似(图1):5个本应该在不同线程中并行执行的操作最终却是在相同的线程(实际上就是UI线程)中串行执行的。这五个并行处理操作可以看成是并发请求对应的5个服务操作。这种串行化执行并发请求的服务操作时如何产生的呢?敬请关注《下篇》。
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。