第七篇:并发模型与实例模型

在以往使用WebService时,针对每一个请求,服务类总是并发响应的,并且对每个请求都生成新的实例。在WCF中,情况发生变化了,它允许服务发布者自定义并组合并发模型与实例模型。

并发模型有三种:

ConcurrencyMode

Single:   单线程模型,可以理解为,针对一个客户端,只有一个线程负责响应;
Reentrant:可重入的单线程模型,与Single的区别在于,对于OneWay/回调,它不会阻塞,而是把回调的线程放到队列尾部等着最后处理;
Multiple: 多线程模型,可以理解为,针对一个客户端,也允许并发访问;

 

实例模型也有三种:

InstanceContextMode

PerCall:   针对每次调用都生成新的服务实例;
PerSession:针对一个会话生成一个服务实例;
Single:    针对所有会话和所有调用共用同一个服务实例;

组合起来是什么效果呢?我们来用示例代码验证一下。

1、服务端

服务契约,具体解释见实现类吧,只要注意一下Sleep方法定义成了IsOneWay=true:

    
    
    
    
  1. using System; 
  2. using System.ServiceModel; 
  3. using System.ServiceModel.Description; 
  4.  
  5. namespace Server 
  6.     [ServiceContract(Namespace = "WCF.Demo")] 
  7.     public interface IData 
  8.     { 
  9.         [OperationContract] 
  10.         int GetCounter(); 
  11.  
  12.         [OperationContract(IsOneWay = true)] 
  13.         void Sleep(); 
  14.     } 


契约实现类:

    
    
    
    
  1. using System; 
  2. using System.ServiceModel; 
  3. using System.ServiceModel.Description; 
  4. using System.Threading; 
  5. using System.Net; 
  6.  
  7. namespace Server 
  8.     //Single并发模式 + PerCall实例模式,针对后面的测试要修改这两个值的组合 
  9.     [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single, InstanceContextMode = InstanceContextMode.PerCall)] 
  10.     public class DataProvider : IData 
  11.     { 
  12.         //定义一个计数器,对每个新生成的服务实例,它都是0,我们通过它来判断是否新实例 
  13.         public int Counter { getset; } 
  14.  
  15.         //获取计数器,并自增计数器 
  16.         public int GetCounter() 
  17.         { 
  18.             return ++Counter; 
  19.         } 
  20.  
  21.         //睡眠2秒 
  22.         public void Sleep() 
  23.         { 
  24.             Thread.Sleep(2000); 
  25.         } 
  26.     } 

App.config不列了,用的是netTcpBinding。


2、客户端:

别的不列了,只列一下调用代码:

    
    
    
    
  1. using System; 
  2. using System.ServiceModel; 
  3. using System.ServiceModel.Channels; 
  4. using System.Threading; 
  5.  
  6. namespace Client 
  7.     class Program 
  8.     { 
  9.         static void Main(string[] args) 
  10.         { 
  11. //启动3个线程并发访问 
  12.             for(int i = 0; i < 3; ++i) 
  13.             { 
  14.                 var thread = new Thread(() => 
  15.                 { 
  16.                     string name = Thread.CurrentThread.Name; 
  17.  
  18.                     var proxy = new ChannelFactory("DataProvider").CreateChannel(); 
  19.  
  20. //先调用GetCounter方法,再调用Sleep方法,然后再调一次GetCounter方法
  21.                     Console.WriteLine(string.Format("{0}: {1}  {2}", name, proxy.GetCounter(), DateTime.Now.ToString("HH:mm:ss.fff")));
  22.   proxy.Sleep(); 
  23.                     Console.WriteLine(string.Format("{0}: {1}  {2}", name, proxy.GetCounter(), DateTime.Now.ToString("HH:mm:ss.fff"))); 
  24.                     ((IChannel)proxy).Close(); 
  25.                 }); 
  26.  
  27. //定义一下线程名,方便识别 
  28.                 thread.Name = "线程" + i; 
  29.                 thread.Start(); 
  30.             } 
  31.         } 
  32.     } 

 

OK,开始验证:

1、ConcurrencyMode.Single + InstanceContextMode.PerCall

执行结果如下:

    
    
    
    
  1. 线程1: 1  15:56:05.262 
  2. 线程2: 1  15:56:05.262 
  3. 线程0: 1  15:56:05.263 
  4. 线程1: 1  15:56:07.263 
  5. 线程2: 1  15:56:07.263 
  6. 线程0: 1  15:56:07.264 

首先,打印出的Counter全是1,说明针对每次请求,服务端的契约实现类(DataProvider)都是新实例化的。其次,同一个线程的两次GetCounter请求相隔了2秒,说明针对一个客户端的调用阻塞了。再次,三个线程几乎同时完成调用,说明它们之间并未互相阻塞。

2、ConcurrencyMode.Single + InstanceContextMode.PerSession

执行结果如下:

    
    
    
    
  1. 线程0: 1  16:02:46.173 
  2. 线程1: 1  16:02:46.173 
  3. 线程2: 1  16:02:46.173 
  4. 线程1: 2  16:02:48.174 
  5. 线程2: 2  16:02:48.174 
  6. 线程0: 2  16:02:48.174 

与上面相比,区别在于同一个线程的Counter在第二次调用时变成2了,说明针对同一个客户端的两次调用使用的是同一个服务实例。

3、ConcurrencyMode.Single + InstanceContextMode.Single

执行结果如下:

    
    
    
    
  1. 线程1: 2  16:05:46.270 
  2. 线程0: 1  16:05:46.270 
  3. 线程2: 3  16:05:46.270 
  4. 线程1: 4  16:05:52.273 
  5. 线程0: 5  16:05:52.273 
  6. 线程2: 6  16:05:52.274 

与上面相比,区别在于Counter一直在增长,这说明在服务端自始至终只有一个服务实例,它来响应所有的会话所有的请求。

4、ConcurrencyMode.Reentrant + InstanceContextMode.PerCall

执行结果如下:

    
    
    
    
  1. 线程1: 1  16:07:42.505 
  2. 线程2: 1  16:07:42.506 
  3. 线程2: 1  16:07:42.507 
  4. 线程1: 1  16:07:42.507 
  5. 线程0: 1  16:07:42.505 
  6. 线程0: 1  16:07:42.507 

和1的区别在于两次GetCounter调用之间没有2秒的延迟,这是由于Reentrant模式下,回调被放入队列尾部再处理,不会阻塞后面的调用。并且针对同一客户端的每个请求都是不同的服务实例在处理,不会阻塞。

5、ConcurrencyMode.Reentrant + InstanceContextMode.PerSession

执行结果如下:

    
    
    
    
  1. 线程2: 1  16:27:44.699 
  2. 线程0: 1  16:27:44.700 
  3. 线程1: 1  16:27:44.699 
  4. 线程0: 2  16:27:46.700 
  5. 线程1: 2  16:27:46.700 
  6. 线程2: 2  16:27:46.700 

与上面相比,区别在于又有了2秒的阻塞,这是由于针对一个客户端的多次请求,是同一个服务实例在处理,虽然允许重入,但只有一个对象,执行顺序是:第一次GetCounter->Sleep(不阻塞)->Sleep回调->第二次GetCounter,所以表现上还是阻塞住了。

6、ConcurrencyMode.Reentrant + InstanceContextMode.Single

执行结果如下:

    
    
    
    
  1. 线程0: 1  16:34:01.417 
  2. 线程1: 3  16:34:01.418 
  3. 线程2: 2  16:34:01.417 
  4. 线程0: 4  16:34:05.420 
  5. 线程1: 5  16:34:07.420 
  6. 线程2: 6  16:34:07.420 

自始至终只有一个服务实例,执行顺序应该是:线程0的GetCounter->线程2的GetCounter->线程1的GetCounter->线程0的Sleep(不阻塞)->线程2的Sleep(不阻塞)->线程0的Sleep回调->线程1的Sleep(不阻塞)->线程0的GetCounter->线程2的Sleep回调->线程1的Sleep回调->线程1的GetCounter->线程2的GetCounter。挺晕的。

7、ConcurrencyMode.Multiple + InstanceContextMode.PerCall

执行结果如下:

    
    
    
    
  1. 线程2: 1  17:07:05.639 
  2. 线程1: 1  17:07:05.639 
  3. 线程0: 1  17:07:05.639 
  4. 线程2: 1  17:07:05.640 
  5. 线程1: 1  17:07:05.641 
  6. 线程0: 1  17:07:05.641 

多次调用完全是并发的,每次调用的实例也是新创建的。

8、ConcurrencyMode.Multiple + InstanceContextMode.PerSession

执行结果如下:

    
    
    
    
  1. 线程1: 1  17:09:10.285 
  2. 线程0: 1  17:09:10.285 
  3. 线程1: 2  17:09:10.286 
  4. 线程0: 2  17:09:10.286 
  5. 线程2: 1  17:09:10.285 
  6. 线程2: 2  17:09:10.287 

多次调用完全并发,但针对同一个会话,实例是相同的。

9、ConcurrencyMode.Multiple + InstanceContextMode.Single

执行结果如下:

    
    
    
    
  1. 线程1: 1  17:16:46.543 
  2. 线程0: 3  17:16:46.543 
  3. 线程2: 2  17:16:46.543 
  4. 线程1: 4  17:16:46.544 
  5. 线程0: 5  17:16:46.544 
  6. 线程2: 6  17:16:46.544 

完全并发,Counter也一直增长,表明自始至终是同一个服务实例。