[原創]WCF後續之旅(12): 執行緒關聯性(Thread Affinity)對WCF併發訪問的影響

weixin_34262482發表於2008-08-25

在本系列的上一篇文章中,我們重點討論了執行緒關聯性對service和callback的操作執行的影響:在service host的時候,可以設定當前執行緒的SynchronizationContext,那麼在預設情況下,service操作的執行將在該SynchronizationContext下執行(也就將service操作包裝成delegate傳入SynchronizationContext的Send或者Post方法);同理,對於Duplex同行方式來講,在client呼叫service之前,如果設定了當前執行緒的SynchronizationContext,callback操作也將自動在該SynchronizationContext下執行。

對於Windows Form Application來講,由於UI Control的操作執行只能在control被建立的執行緒中被操作,所以一這樣的方式實現了自己的SynchronizationContext(WindowsFormsSynchronizationContext):將所有的操作Marshal到UI執行緒中。正因為如此,當我們通過Windows Form Application進行WCF service的host的時候,將會對service的併發執行帶來非常大的影響。

詳細講,由於WindowsFormsSynchronizationContext的Post或者Send方法,會將目標方法的執行傳到UI主執行緒,所以可以說,所有的service操作都在同一個執行緒下執行,如果有多個client的請求同時抵達,他們並不能像我們希望的那樣併發的執行,而只能逐個以序列的方式執行。(Source Code從這裡下載)

一、通過例項證明執行緒關聯性對併發的影響

我們可以通過一個簡單的例子證明:在預設的情況下,當我們通過Windows Form Application進行service host的時候,service的操作都是在同一個執行緒中執行的。我們照例建立如下的四層結構的WCF service應用:

image

1、Contract:IService

   1: namespace Artech.ThreadAffinity2.Contracts
   2: {
   3:     [ServiceContract]
   4:     public interface IService
   5:     {
   6:         [OperationContract]
   7:         void DoSomething();
   8:     }
   9: }
2、Service:Service

   1: namespace Artech.ThreadAffinity2.Services
   2: {
   3:     public class Service:IService
   4:     {
   5:         public static ListBox DispalyPanel
   6:         { get; set; } 
   7:  
   8:         public static SynchronizationContext SynchronizationContext
   9:         { get; set; } 
  10:  
  11:         #region IService Members 
  12:  
  13:         public void DoSomething()
  14:         {
  15:             Thread.Sleep(5000);
  16:             int threadID = Thread.CurrentThread.ManagedThreadId;
  17:             DateTime endTime = DateTime.Now;
  18:             SynchronizationContext.Post(delegate
  19:             {
  20:                 DispalyPanel.Items.Add(string.Format("Serice execution ended at {0}, Thread ID: {1}",
  21:                     endTime, threadID));
  22:             }, null);
  23:         } 
  24:  
  25:         #endregion
  26:     }
  27: } 

為了演示對併發操作的影響,在DoSomething()中,我將執行緒休眠10s以模擬一個相對長時間的操作執行;為了能夠直觀地顯示操作執行的執行緒和執行完成的時間,我將他們都列印在host該service的Windows Form的ListBox中,該ListBox通過static property的方式在host的時候指定。並將對ListBox的操作通過UI執行緒的SynchronizationContext(也是通過static property的方式在host的時候指定)的Post中執行(實際上,在預設的配置下,不需要如此,因為service操作的執行始終在Host service的UI執行緒下)。

3、Hosting

我們將service 的host放在一個Windows Form Application的某個一個Form的Load事件中。該Form僅僅具有一個ListBox:

   1: namespace Artech.ThreadAffinity2.Hosting
   2: {
   3:     public partial class HostForm : Form
   4:     {
   5:         private ServiceHost _serviceHost; 
   6:  
   7:         public HostForm()
   8:         {
   9:             InitializeComponent();
  10:         } 
  11:  
  12:         private void HostForm_Load(object sender, EventArgs e)
  13:         {
  14:             this.listBoxResult.Items.Add(string.Format("The ID of the Main Thread: {0}", Thread.CurrentThread.ManagedThreadId));
  15:             this._serviceHost = new ServiceHost(typeof(Service));
  16:             this._serviceHost.Opened += delegate
  17:             { 
  18:                 this.Text = "Service has been started up!";
  19:             };
  20:             Service.DispalyPanel = this.listBoxResult;
  21:             Service.SynchronizationContext = SynchronizationContext.Current;
  22:             this._serviceHost.Open();
  23:         } 
  24:  
  25:         private void HostForm_FormClosed(object sender, FormClosedEventArgs e)
  26:         {
  27:             this._serviceHost.Close();
  28:         }
  29:     }
  30: } 
  31:  

在HostForm_Load,先在ListBox中顯示當前執行緒的ID,然後通過Service.DispalyPanel和Service.SynchronizationContext 為service的執行設定LisBox和SynchronizationContext ,最後將servicehost開啟。下面是Configuration:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>
   4:         <services>
   5:             <service name="Artech.ThreadAffinity2.Services.Service">
   6:                 <endpoint binding="basicHttpBinding" contract="Artech.ThreadAffinity2.Contracts.IService" />
   7:                 <host>
   8:                     <baseAddresses>
   9:                         <add baseAddress="http://127.0.0.1/service" />
  10:                     </baseAddresses>
  11:                 </host>
  12:             </service>
  13:         </services>
  14:     </system.serviceModel>
  15: </configuration> 

4、Client

我們通過一個Console Application來模擬client端程式,先看看configuration:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>
   4:         <client>
   5:             <endpoint address="http://127.0.0.1/service" binding="basicHttpBinding"
   6:                 contract="Artech.ThreadAffinity2.Contracts.IService" name="service" />
   7:         </client>
   8:     </system.serviceModel>
   9: </configuration>
下面是service呼叫的程式碼:
   1: namespace Clients
   2: {
   3:     class Program
   4:     {
   5:         static void Main(string[] args)
   6:         {
   7:             using (ChannelFactory<IService> channelFactory = new ChannelFactory<IService>("service"))
   8:             {
   9:                 IList<IService> channelList = new List<IService>();
  10:                 for (int i = 0; i < 10; i++)
  11:                 {
  12:                     channelList.Add(channelFactory.CreateChannel());
  13:                 } 
  14:  
  15:                 Array.ForEach<IService>(channelList.ToArray<IService>(), 
  16:                     delegate(IService channel)
  17:                 { 
  18:                     ThreadPool.QueueUserWorkItem(
  19:                     delegate
  20:                     {
  21:                         channel.DoSomething();
  22:                         Console.WriteLine("Service invocation ended at {0}", DateTime.Now);
  23:                     }, null);
  24:                 } );
  25:                 Console.Read();
  26:             }
  27:         }
  28:     }
  29: } 
  30:  

首先通過ChannelFactory<IService> 先後建立了10個Proxy物件,然後以非同步的方式進行service的呼叫(為了簡單起見,直接通過ThreadPool實現非同步呼叫),到service呼叫結束將當前時間輸出來。我們來執行一下我們的程式,看看會出現怎樣的現象。先來看看service端的輸出結果:

image

通過上面的結果,從執行的時間來看service執行的並非併發,而是序列;從輸出的執行緒ID更能說明這一點:所有的操作的執行都在同一個執行緒中,並且service執行的執行緒就是host service的UI執行緒。這充分證明了service的執行具有與service host的執行緒關聯性。通過Server端的執行情況下,我們不難想象client端的執行情況。雖然我們是以非同步的方式進行了10次service呼叫,但是由於service的執行並非併發執行,client的執行結果和同步下執行的情況並無二致:

image

二、解除執行緒的關聯性

在本系列的上一篇文章,我們介紹了service的執行緒關聯性通過ServiceBeahavior的UseSynchronizationContext控制。UseSynchronizationContext實際上代表的是是否使用預設的SynchronizationContext(實際上是DispatchRuntime的SynchronizationContext屬性中制定的)。我們對service的程式碼進行如下簡單的修改,使service執行過程中不再使用預設的SynchronizationContext。

   1: namespace Artech.ThreadAffinity2.Services
   2: {
   3:     [ServiceBehavior(UseSynchronizationContext = false)]    
   4:     public class Service:IService
   5:     {
   6:  
   7:          //...
   8:     }
   9: }

再次執行我們的程式,看看現在具有怎樣的表現。首先看server端的輸出結果:

image

我們可以看出,service的執行並不在service host的主執行緒下,因為Thread ID不一樣,從時間上看,也可以看出它們是併發執行的。從Client的結果也可以證明這一點:

image 

結論:當我們使用Windows Form Application進行service host的時候,首先應該考慮到在預設的情況下具有執行緒關聯特性。你需要評估的service的整個操作是否真的需要依賴於當前UI執行緒,如果不需要或者只有部分操作需要,將UseSynchronizationContext 設成false,將會提高service處理的併發量。對於依賴於當前UI執行緒的部分操作,可以通過SynchronizationContext實現將操作Marshal到UI執行緒中處理,對於這種操作,應該盡力那個縮短執行的時間。

WCF後續之旅:
WCF後續之旅(1): WCF是如何通過Binding進行通訊的
WCF後續之旅(2): 如何對Channel Layer進行擴充套件——建立自定義Channel
WCF後續之旅(3): WCF Service Mode Layer 的中樞—Dispatcher
WCF後續之旅(4):WCF Extension Point 概覽
WCF後續之旅(5): 通過WCF Extension實現Localization
WCF後續之旅(6): 通過WCF Extension實現Context資訊的傳遞
WCF後續之旅(7):通過WCF Extension實現和Enterprise Library Unity Container的整合
WCF後續之旅(8):通過WCF Extension 實現與MS Enterprise Library Policy Injection Application Block 的整合
WCF後續之旅(9):通過WCF的雙向通訊實現Session管理[Part I]
WCF後續之旅(9): 通過WCF雙向通訊實現Session管理[Part II]
WCF後續之旅(10): 通過WCF Extension實現以物件池的方式建立Service Instance
WCF後續之旅(11): 關於併發、回撥的執行緒關聯性(Thread Affinity)
WCF後續之旅(12): 執行緒關聯性(Thread Affinity)對WCF併發訪問的影響
WCF後續之旅(13): 建立一個簡單的WCF SOAP Message攔截、轉發工具[上篇]
WCF後續之旅(13):建立一個簡單的SOAP Message攔截、轉發工具[下篇]
WCF後續之旅(14):TCP埠共享
WCF後續之旅(15): 邏輯地址和實體地址
WCF後續之旅(16): 訊息是如何分發到Endpoint的--訊息篩選(Message Filter)
WCF後續之旅(17):通過tcpTracer進行訊息的路由

相關文章