一、概述
传入的客户端调用消息会分发给Windows I/O线程池(线程默认为1000)上的服务实例。多个客户端可以发起多个并发的调用,并且服务可以在多个线程上处理这些请求。如果传入的调用分发给同一个服务实例,就必须对内存中的服务状态提供线程安全的访问方式,否则,可能存在错误的风险。客户端回调时的内存状态亦是如此,因为回调也是分发给I/O线程池上的线程。此外,为了同步访问实例的状态,还需要同步访问实例之间共享的资源,比如静态变量。
WCF提供了两种同步模式:自动同步与手动同步。虽然自动同步会指导WCF去同步访问服务实例,但是它只对服务和回调类可用。手工同步是让开发者来控制同步,需要应用程序具体的集成工作。开发者需要使用.NET同步锁。手工同步的优点是,对服务类和非服务类来说,它都可以用,而且它允许开发者去优化吞吐量和伸缩性。
二、实例管理和并发
服务实例线程安全性与服务实例化模式紧密相关。
根据定义,单调服务实例是安全的,因为每个调用都有自己的服务实例。实例只分配给自己的工作线程访问,不必考虑同步问题。但是,单调服务是状态可知的,可以将状态存储在内部资源里,比如静态字典里,可以被多个线程访问,因为服务可以使用并发调用,不论是来自相同的客户端还是不同的客户端。
会话服务需要并发管理和同步,因为客户端可能使用相同的代理或者通过多线程发送请求给服务。
对于并发访问单例服务才是最敏感,而且它必须要支持同步访问。单例服务有一些所有客户端共享的状态信息。
发生客户端多线程调用的最大可能是,会话和单例中有多个客户端在不同的执行上下文里运行,每个客户端都使用自己的线程去调用服务。
但所有的调用都会通过I/O线程池上的线程进入相同的服务实例中——本质上,这就需要同步机制。
三、服务并发模式
ServiceBehavior的ConcurrencyMode属性控制着服务实例的并发访问。
// 指定服务类是支持单线程还是多线程操作模式。
public enum ConcurrencyMode
{
// 默认,单线程模型
Single = 0,
// 单线程,可重入模型,通常用在CallBack调用模型中
Reentrant = 1,
// 多线程模型
Multiple = 2,
} [AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : ...
{
public ConcurrencyMode ConcurrencyMode
{get;set;}
//More members
}枚举值ConcurrencyMode控制着是否以及何时允许并发调用。ConcurrencyMode的名字是错误的,正确的名字应该是ConcurrencyContextMode,因为它同步访问的不是实例,而是包含实例的上下文(InstanceContextMode也控制这上下文的实例化,而不是实例)。
1、ConcurrencyMode.Single
当并发模式配置为ConcurrencyMode.Single时,WCF会自动对服务上下文提供同步机制,会使用同步锁来把上下文和服务实例关联起来以阻止并发调用。每次进入的调用必须获得同步锁。一旦操作返回,WCF就会释放同步锁,从而允许后来的调用者进入。
同一个服务实例不会同时处理多个请求。当服务在处理请求时会对当前服务加锁,如果再有其它请求需要该服务处理的时候,需要排队等候。
例如:我们去银行去办理业务,如果营业厅中只有一个窗口对外服务的话,那当前窗口每次只能处理一个用户请求,如果再有其它用户需要办理业务的话,只能排队等待。直到我办理完业务后,营业窗口才能为队列中下个用户提供服务。
2、ConcurrencyMode.Reentrant
可重入的单线程处理模式,它仍然是单线程处理。服务端一次仍然只能处理一个请求,如果有多个请求同时到达仍然需要排队。与单线程不同的是,请求在处理过程中可以去调用其它服务,等到其它服务处理完成后,再回到原服务等待队列尾排队。在调用其它服务的过程中,会暂时释放锁,其它等待线程会趁机进行服务的调用。这种模式常见于服务端回调客户端的场境中。
重入适用于以下情况:
- 如果单例服务调用的服务回调服务实例,那么单例服务调出就会带来死锁问题。
- 在同一个应用程序域中,如果客户端把代理存储咋全局变量中,那么服务调用校友的某些对象就需要使用代理引用,去回调最初的服务。
- 在非单向操作上的调用必须允许重入服务调用
- 如果服务调用执行的事件过长,设置不会重入服务实例,那么要想办法优化吞吐量。在调出期间,允许其它客户端进入服务实例。
例如:我们去银行办理业务,营业厅中还是只有一个窗口对外服务,一次只能处理一个用户请求。我向银行服务员请求办理开户业务,从此刻开始该服务被我锁定,其它人员只能排队等待。在服务员办理业务的过程中,会让我填写开户申请表,这个签字的过程就是服务端对客户端的回调过程。由于填写开户申请表的时间会很长,为了不耽搁后面排队顾客的时间,我暂时释放对服务员的锁定,到旁边填写申请表,让后面的人员办理业务。在我签完字后再回到队列中等待服务,当再轮到我的时候我再次锁定服务,把凭据交给服务员,让他继续处理我的业务,这相当于服务的“重入”过程。 等到业务办完后,我会离开银行柜台,释放对该服务的锁定,等待队列中的下个人员又开始锁定服务办理业务了。
3、ConcurrencyMode.Multiple
多线程模式,多线程模式可以很好地增加系统的吞吐量。当多个用户请求服务实例时,服务并不会加锁,而是同时为多个请求服务。这样一来对所有用户共享的资源就会产生影响,所以这种多线程的访问模式需要对共享资源做好保护,大部份的情况下需我们的手动编写代码来实现多线程之间的访问保护。
例如:我们去吃烤肉串,烤肉串的师傅可以同时为多个顾客烤制,而不用一个一个地排队等待,这就是典型的多线程处理模式。但这种模式如果不对肉串很好保护得的话那可麻烦了,比仿,我要的是3串麻辣味的,你要的是3串香辣味的,他要的是3串原味的,烤肉的时傅在烤制的时候需要对这三份肉串做好保护,防止做出一些“五味俱全”的肉串来。
四、实例与并发访问
使用相同的代理,一个单独的客户端可以像服务端发起多个并发的调用请求。客户端可以多线程去激活调用,也可以在同一个线程上发起快速、连续的单向调用。同一客户端的请求是否被服务并发处理完全取决与服务的实例模式、服务配置的并发模式以及传输模式(传输会话)。
单调服务
对于单调服务,如果没有传输层会话,那么并发处理是允许的。调用请求在到达服务端后会分发给每个新的服务实例,然后并发执行。这是一种可以不管服务并发模式的情况。
如果单调服务使用传输层会话,那么是否并发处理客户端请求就取决于服务配置的并发模式。如果服务配置为ConcurrentcyMode.Single,就不允许使用并发处理,每次只会分发一个调用消息。原因是使用了ConcurrentcyMode.Single,WCF会维护传输层会话,这样消息会严格按照发送的顺序执行。应该避免使用过久的处理,因为可能会导致超时。
如果服务配置为ConcurrentcyMode.Multiple,那么是允许并发的。调用会在到达时立即分发给新的服务实例进行并发处理。它的好处就是吞吐量,使用ConcurrentcyMode.Multiple搭配单调服务是个好主意——实例本身是线程安全的,所以不需要额外使用并发机制。
如果服务配置了ConcurrencyMode.Reetrant且服务没有调出,则结果就与ConcurrentcyMode.Single一样。
会话与单例服务
在使用会话和单例服务时,并发模式会控制等待调用的并发执行工作。如果服务配置了ConcurrentcyMode.Single并发模式,那么每次会发送给服务实例一个调用消息,等待的调用会放进一个队列中。应该避免使用过久的处理,因为可能会导致超时。
如果服务并发模式配置为ConcurrencyMode.Multiple,那么允许同时处理来自同一客户端的并发调用。调用消息在到达服务端时就会立即处理(取决于限流设置)。当然,还要处理服务实例的同步访问问题,否则可能带来一些风险。
如果服务并发模式配置为ConcurrencyMode.Reetant,那么服务实例的行为就与ConcurrentcyMode.Single类似。但是,如果服务调出,下一个调用就可以继续执行。