在本系列的上一篇文章中,我們重點討論了執行緒關聯性對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應用:
1、Contract:IService
1: namespace Artech.ThreadAffinity2.Contracts
2: {
3: [ServiceContract]
4: public interface IService
5: {
6: [OperationContract]
7: void DoSomething();
8: }
9: }
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>
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端的輸出結果:
通過上面的結果,從執行的時間來看service執行的並非併發,而是序列;從輸出的執行緒ID更能說明這一點:所有的操作的執行都在同一個執行緒中,並且service執行的執行緒就是host service的UI執行緒。這充分證明了service的執行具有與service host的執行緒關聯性。通過Server端的執行情況下,我們不難想象client端的執行情況。雖然我們是以非同步的方式進行了10次service呼叫,但是由於service的執行並非併發執行,client的執行結果和同步下執行的情況並無二致:
二、解除執行緒的關聯性
在本系列的上一篇文章,我們介紹了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端的輸出結果:
我們可以看出,service的執行並不在service host的主執行緒下,因為Thread ID不一樣,從時間上看,也可以看出它們是併發執行的。從Client的結果也可以證明這一點:
結論:當我們使用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進行訊息的路由