閱讀985 返回首頁    go 阿裏雲 go 技術社區[雲棲]


ConcurrencyMode.Multiple模式下的WCF服務就一定是並發執行的嗎:探討同步上下文對並發的影響[上篇]

在《並發與實例上下文模式》中,我們通過實例演示的方式講述了基於不同實例上下文模式的並發行為。對於這個實例中的服務類型CalculatorService,讀者應該還記得我們對它進行了特別的定義:通過ServiceBehaviorAttribute特性將屬性將UseSynchronizationContext設置成False。至於為何要這麼做,這就是本篇文章需要為你講述的內容。為了讓讀者對本節介紹的內容有一個深刻的認識,我們不然去掉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相同)。

image 圖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所有成員中,最重要的就是SendPost兩個方法。調用者調用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所示的輸出結果。

image 圖2 並行操作在相同SynchronizationContext中執行結果

圖2反映出來的結果與上麵我們去除掉應用在CalculatorServiceAttribute的UseSynchronizationContext屬性定義後服務端得到的監控結果比較類似(圖1):5個本應該在不同線程中並行執行的操作最終卻是在相同的線程(實際上就是UI線程)中串行執行的。這五個並行處理操作可以看成是並發請求對應的5個服務操作。這種串行化執行並發請求的服務操作時如何產生的呢?敬請關注《下篇》


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 15:04:48

  上一篇:go  回調與並發: 通過實例剖析WCF基於ConcurrencyMode.Reentrant模式下的並發控製機製
  下一篇:go  ConcurrencyMode.Multiple 模式下的WCF服務就一定是並發執行的嗎:探討同步上下文對並發的影響[下篇]