在本系列的上一篇文章中,我們重點討論了線程關聯性對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的請求同時抵達,他們並不能像我們希望的那樣並發的執行,而只能逐個以串行的方式執行。
一、通過實例證明線程關聯性對並發的影響
我們可以通過一個簡單的例子證明:在默認的情況下,當我們通過Windows Form Application進行service host的時候,service的操作都是在同一個線程中執行的。我們照例創建如下的四層結構的WCF service應用:
1、Contract:IService
namespace Artech.ThreadAffinity2.Contracts
{
[ServiceContract]
public interface IService
{
[OperationContract]
void DoSomething();
}
}
2、Service:Service
namespace Artech.ThreadAffinity2.Services
{
public class Service:IService
{
public static ListBox DispalyPanel
{ get; set; }
public static SynchronizationContext SynchronizationContext
{ get; set; }
#region IService Members
public void DoSomething()
{
Thread.Sleep(5000);
int threadID = Thread.CurrentThread.ManagedThreadId;
DateTime endTime = DateTime.Now;
SynchronizationContext.Post(delegate
{
DispalyPanel.Items.Add(string.Format("Serice execution ended at {0}, Thread ID: {1}",
endTime, threadID));
}, null);
}
#endregion
}
}
為了演示對並發操作的影響,在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:
namespace Artech.ThreadAffinity2.Hosting
{
public partial class HostForm : Form
{
private ServiceHost _serviceHost;
public HostForm()
{
InitializeComponent();
}
private void HostForm_Load(object sender, EventArgs e)
{
this.listBoxResult.Items.Add(string.Format("The ID of the Main Thread: {0}", Thread.CurrentThread.ManagedThreadId));
this._serviceHost = new ServiceHost(typeof(Service));
this._serviceHost.Opened += delegate
{
this.Text = "Service has been started up!";
};
Service.DispalyPanel = this.listBoxResult;
Service.SynchronizationContext = SynchronizationContext.Current;
this._serviceHost.Open();
}
private void HostForm_FormClosed(object sender, FormClosedEventArgs e)
{
this._serviceHost.Close();
}
}
}
在HostForm_Load,先在ListBox中顯示當前線程的ID,然後通過Service.DispalyPanel和Service.SynchronizationContext 為service的執行設置LisBox和SynchronizationContext ,最後將servicehost打開。下面是Configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="Artech.ThreadAffinity2.Services.Service">
<endpoint binding="basicHttpBinding" contract="Artech.ThreadAffinity2.Contracts.IService" />
<host>
<baseAddresses>
<add baseAddress="http://127.0.0.1/service" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
4、Client
我們通過一個Console Application來模擬client端程序,先看看configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint address="http://127.0.0.1/service" binding="basicHttpBinding"
contract="Artech.ThreadAffinity2.Contracts.IService" name="service" />
</client>
</system.serviceModel>
</configuration>
下面是service調用的代碼:
namespace Clients
{
class Program
{
static void Main(string[] args)
{
using (ChannelFactory<IService> channelFactory = new ChannelFactory<IService>("service"))
{
IList<IService> channelList = new List<IService>();
for (int i = 0; i < 10; i++)
{
channelList.Add(channelFactory.CreateChannel());
}
Array.ForEach<IService>(channelList.ToArray<IService>(),
delegate(IService channel)
{
ThreadPool.QueueUserWorkItem(
delegate
{
channel.DoSomething();
Console.WriteLine("Service invocation ended at {0}", DateTime.Now);
}, null);
} );
Console.Read();
}
}
}
}
首先通過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。
namespace Artech.ThreadAffinity2.Services
{
[ServiceBehavior(UseSynchronizationContext = false)]
public class Service:IService
{
……
}
}
再次運行我們的程序,看看現在具有怎樣的表現。首先看server端的輸出結果:
我們可以看出,service的執行並不在service host的主線程下,因為Thread ID不一樣,從時間上看,也可以看出它們是並發執行的。從Client的結果也可以證明這一點:
結論:當我們使用Windows Form Application進行service host的時候,首先應該考慮到在默認的情況下具有線程關聯特性。你需要評估的service的整個操作是否真的需要依賴於當前UI線程,如果不需要或者只有部分操作需要,將UseSynchronizationContext 設成false,將會提高service處理的並發量。對於依賴於當前UI線程的部分操作,可以通過SynchronizationContext實現將操作Marshal到UI線程中處理,對於這種操作,應該盡力那個縮短執行的時間。
本文配套源碼