在本系列的上一篇文章中,我们重点讨论了线程关联性对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: }
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端的输出结果:
通过上面的结果,从执行的时间来看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进行消息的路由
作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。