WCF技术剖析之二十三:服务实例(Service Instance)生命周期如何控制[下篇]

[第2篇]中,我们深入剖析了单调(PerCall)模式下WCF对服务实例生命周期的控制,现在我们来讨轮另一种极端的服务实例上下文模式:单例(Single)模式。在单例模式下,WCF通过创建一个唯一的服务实例来处理所有的客户端服务调用请求。这是一个极端的服务实例激活方式,由于服务实例的唯一性,所有客户端每次调用的状态能够被保存下来,但是当前的状态是所有客户端作用于服务实例的结果,而不能反映出具体某个客户端多次调用后的状态。WCF是一个典型的多线程的通信框架,对并发的服务调用请求是最基本的能力和要求,但是服务实例的单一性就意味着相同服务实例需要在多个线程下并发地调用。

一、实例演示:演示服务实例的单一性

为了让读者对单例实例上下文模式有一个直观的认识,我们通过一个简单的案例演示单例模式下服务实例的单一性。这里使用前面章节使用过的累加器的例子,下面是服务契约和服务实现的定义:在初始化时,运算的结果为零,通过Add方法仅仅对结果累加,计算的结果通过GetResult操作返回。在CalculatorService上面,通过System.ServiceModel.ServiceBehaviorAttribute将服务设定为单例模式。

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {
   4:     [ServiceContract(Namespace="http://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         void Add(double x);
   9:         [OperationContract]
  10:         double GetResult();
  11:     }
  12: }
   1: using System.ServiceModel;
   2: using Artech.WcfServices.Contracts;
   3: namespace Artech.WcfServices.Services
   4: {
   5:     [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
   6:     public class CalculatorService : ICalculator
   7:     {
   8:         private double _result;
   9:         public void Add(double x)
  10:         {
  11:             this._result += x;
  12:         }
  13:         public double GetResult()
  14:         {
  15:             return this._result;
  16:         }
  17:     }
  18: }

在客户端,通过ChannelFactory<ICalculator>创建两个服务代理,模拟两个不同的客户端。从最终输出来看,得到的结果并不能反映出具体某个客户端正常的累加运算(对于通过calculator2模拟的客户端,仅仅调用了一次Add(3),得到的结果却是6)这是所有客户端一起累加的结果,这就是服务实例的单一性造成。

   1: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
   2: {
   3:     ICalculator calculator1 = channelFactory.CreateChannel();
   4:     ICalculator calculator2 = channelFactory.CreateChannel();
   5:  
   6:     Console.WriteLine("1st serivce proxy:");
   7:     Console.WriteLine("Add(3);");
   8:     calculator1.Add(3);
   9:     Console.WriteLine("The result is {0}.\n", calculator1.GetResult());
  10:  
  11:     Console.WriteLine("2nd serivce proxy:");
  12:     Console.WriteLine("Add(3);");
  13:     calculator2.Add(3);
  14:     Console.WriteLine("The result is {0}.", calculator2.GetResult());
  15: } 

输出结果:

1st serivce proxy:
Add(3);
The result is 3.
 
2nd serivce proxy:
Add(3);
The result is 6.

二、 单例模式下服务实例上下文提供机制

与其他两种实例上下文模式(单调模式和会话模式)相比,单例模式具有不一样的服务实例创建方式。从服务实例创建的时机来看,单调服务实例创建于每一个服务调用,会话服务实例则创建于服务代理的显式开启或第一个服务调用,而单例服务实例则在服务寄宿之时。对于单例模式,既可以通过WCF提供的实例激活机制自动创建服务实例,也可以将创建好的对象作为服务实例,我们把这两种服务实例的提供方式分别称为隐式单例(Hidden Singleton)和已知单例(Well-Known Singleton)。

1、已知单例(Well-Known Singleton)与隐式单例(Hidden Singleton)

一般地,在寄宿某个服务的时候,我们会指定服务的类型。WCF会根据服务类型,通过反射的机制,调用默认无参构造函数创建服务实例。但是,如果服务类型没有定义无参构造函数,或者我们须要手工对服务实例作一些初始化工作,WCF提供的实例激活机制就不能为我们服务了。为了解决这种需求,须要自行创建服务实例,采用基于服务实例的寄宿方式来代替原来基于服务类型的寄宿方式。只有单例实例上下文模式才能采用这种寄宿方式,我们把这种基于现有服务对象的服务实例提供模式称为“已知单例(Well-Konown Singletone)模式”。可以利用ServiceHost下面一个构造函数重载来实现基于已知单例的服务寄宿。

public class ServiceHost : ServiceHostBase
{
    //其他成员
    public ServiceHost(object singletonInstance, params Uri[] baseAddresses); 
}
   1: CalculatorService calculatorService = new CalculatorService();
   2: using (ServiceHost host = new ServiceHost(calculatorService, new Uri("http://127.0.0.1:9999/calculatorservice")))
   3: {
   4:     host.Open();
   5:     Console.Read();
   6: } 

通过上述方法设置已知的单例服务对象,可以通过 ServiceHost的只读属性SingletonInstance获得。而对于服务的ServiceHost的获取,可以通过当前OperationContext的只读属性Host得到。(通过OperationContext的Host只读属性获得的是ServiceHostBase对象,如果没有使用到自定义的ServiceHostBase,通过该属性获得的是ServiceHost对象)。下面的代理列出了相关的API和编程方式:

   1: public class ServiceHost : ServiceHostBase
   2: {   
   3:     //其他成员
   4:     public object SingletonInstance { get; }   
   5: } 
   1: public sealed class OperationContext : IExtensibleObject<OperationContext>
   2: {
   3:     //其他成员
   4:     public static OperationContext Current { get; set; }
   5:     public ServiceHostBase Host { get; }
   6: } 
   1: ServiceHost host = OperationContext.Current.Host as ServiceHost;
   2: if (host != null)
   3: {
   4:     CalculatorService singletonService = host.SingletonInstance as CalculatorService;
   5: } 

对于单例实例上下文模式,如果采用传统的基于服务类型的寄宿方式,即通过服务类型而非服务实例创建ServiceHost对象,服务实例是通过WCF内部的服务实例激活机制创建的。不同于其他两种实例上下文模式采用请求式实例激活方式(单调实例上下文在处理每次调用请求时创建,而会话实例上下文模式则在接收到某个客户端的第一次调用请求时创建服务实例上下文),单例实例上下文在ServiceHost的初始化过程中被创建。我们把这种模式称为隐式单例模式。

在《WCF技术剖析(卷1)》第7章介绍服务寄宿的时候,我们谈到整个服务的寄宿过程大体分为两个阶段:ServiceHost的初始化和ServiceHost的开启。第一个阶段的主要目的在于通过对服务类型的反射,以及对配置的解析,创建用于表示当前寄宿服务的ServiceDescription对象,而隐式单例服务对象就创建于这个阶段。

当基于单例服务的ServiceHost被成功创建并被初始化后,服务描述(通过类型System.ServiceModel.Description.ServiceDescription表述)被创建出来。阅读了第7章的读者应该很清楚,ServiceDescription有一个Behaviors属性维护着服务所有服务行为。通过自定义特性设置的ServiceBehaviorAttribute作为最常见的一种服务的行为自然也在其中。在服务寄宿过程中指定的已知服务实例,和WCF创建的隐式服务实例则分别保存在ServiceBehaviorAttribute的两个私有变量之中。

   1: public class ServiceDescription
   2: {
   3:     //其他成员
   4:     public KeyedByTypeCollection<IServiceBehavior> Behaviors { get; }
   5: }
   1: [AttributeUsage(AttributeTargets.Class)]
   2: public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
   3: {
   4:     //其他成员
   5:     private object hiddenSingleton;   
   6:     private object wellKnownSingleton;  
   7: }

2、单例服务实例上下文的实现

在WCF服务端运行时中,服务实例本身并不孤立地存在,而是被封装到一个System.ServiceModel.InstanceContext对象之中。现在就来讨论用于封装单例服务对象的实例上下文是如何创建的。

与隐式单例服务实例一样,封装服务实例的服务实例上下文的创建过程也是发生在服务的寄宿过程中。不过,前者是发生在ServiceHost的创建和初始化阶段,而后者则是发生在ServiceHost的开启过程中。第7章曾经详细介绍了ServiceHost开启的整个流程。对此还有印象的读者应该会记得,最后一个步骤是“应用分发行为(Apply Dispatching Behavior)”。在这个步骤中,WCF会遍历当前服务相关的所有行为,不仅仅包括服务行为,也包括终结点行为、契约行为和操作行为,调用它们的ApplyDispatchBehavior方法。而单例服务实例上下文的创建就发生在ServiceBehaviorAttribute的ApplyDispatchBehavior方法被执行的时候。

   1: [AttributeUsage(AttributeTargets.Class)]
   2: public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
   3: {
   4:     //其他成员
   5:     void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase);  
   6: }

具体来讲,在执行ServiceBehaviorAttribute的ApplyDispatchBehavior方法进行服务实例上下文的创建时,如果已知单例服务对象(wellKnownSingleton字段)存在,则根据该对象创建实例上下文,否则实例上下文就根据隐式单例服务对象(hiddenSingleton字段)创建。我们可以通过DispatchRuntime的SingletonInstanceContext属性进行设置并获取单例服务实例上下文,SingletonInstanceContext属性在DispatchRuntime的定义如下:

   1: public sealed class DispatchRuntime
   2: {
   3:     //其他成员
   4:     public InstanceContext SingletonInstanceContext { get; set; }  
   5: }

DispatchRuntime代表WCF服务的运行时,在服务寄宿时被创建,引用着绝大部分用于消息分发、实例激活、操作执行相关的运行时组件。DispatchRuntime是一个全局性的对象,与当前ServiceHost绑定,只有当ServiceHost关闭或卸载时,DispatchRuntime才会被卸载。也正因为如此,被DispatchRuntime引用的SingletonInstanceContext对象才成为了真正意义上的单例对象,具有了和ServiceHost相同的生命周期。在单例模式下,所有的服务调用请求的处理都是通过一个服务实例来完成的。

三、 单例服务与可扩展性

对并发服务调用请求的处理是WCF最基本要求,为了提供服务的响应能力,WCF会在不同的线程中处理并发请求。在单例模式下,服务实例是唯一的,也就是说相同的服务实例会同时被多个线程并发地访问。在默认的情况下,多个线程以同步的方式访问单例服务对象,也就是说,在某个时刻,最多只会有一个线程在使用服务实例。如果一个服务操作需要1秒,那么在一分钟内最多只能处理60个服务调用请求。倘若客户端采用默认的超时时限(1分钟),对于60个并发地服务调用请求,至少会有一个服务调用会失败。这极大地降低了WCF服务的可扩展性、响应能力和可用性。

为了让读者对单例服务的低可扩展性有一个深刻的认识,我写了一个极端的案例。从这个案例演示中,读者会清晰地认识到提供一个相同的功能,采用单调模式和单例模式,对客户端影响的差别有多大。本案例同样沿用计算服务的例子,Add方法中通过使线程休眠5秒模拟一个耗时的服务操作,下面是服务的定义,采用单调实例上下文模式。

   1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
   2: public class CalculatorService : ICalculator
   3: {    
   4:     public double Add(double x, double y)
   5:     {
   6:         Thread.Sleep(5000);
   7:         return x + y;
   8:     }
   9: }

在客户端,通过ThreadPool模拟5个并发的客户端,在Add操作调用成功后输出当前的时间,从而检验服务的响应能力。

   1: for (int i = 0; i < 5; i++)
   2: {
   3:     ThreadPool.QueueUserWorkItem(delegate
   4:     {
   5:         using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
   6:         {
   7:             ICalculator calculator = channelFactory.CreateChannel();
   8:             Console.WriteLine("{3}: x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2), DateTime.Now);
   9:         }
  10:     });
  11: }

从客户端输出结果我们可以看出,对于5个并发的服务调用均得到了及时的相应,这是我们希望看到的结果。

3/8/2009 08:03:17 : x + y = 3 when x = 1 and y = 2
3/8/2009 08:03:17 : x + y = 3 when x = 1 and y = 2
3/8/2009 08:03:17 : x + y = 3 when x = 1 and y = 2
3/8/2009 08:03:18 : x + y = 3 when x = 1 and y = 2
3/8/2009 08:03:18 : x + y = 3 when x = 1 and y = 2

但是,如果将实例上下文模式换成是InstanceContextMode.Single,情况就完全不一样了。从最终的输出结果可以看出,客户端得到执行结果的间隔为5s,由此可知服务操作在服务端是以同步的方式执行的。

   1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
   2: public class CalculatorService : ICalculator, IDisposable
   3: {
   4:    //省略实现
   5: }

输出结果:

3/8/2009 08:03:25 : x + y = 3 when x = 1 and y = 2

3/8/2009 08:03:30 : x + y = 3 when x = 1 and y = 2

3/8/2009 08:03:35 : x + y = 3 when x = 1 and y = 2

3/8/2009 08:03:40 : x + y = 3 when x = 1 and y = 2

3/8/2009 08:03:45 : x + y = 3 when x = 1 and y = 2

WCF通过并发模式(Concurrency Mode)表示多线程访问单例服务对象的方式,而并发模式作为一种服务行为可以通过ServiceBehaviorAttribute特性进行设定。WCF通过ConcurrencyMode枚举来表示不同形式的并发模式,三个枚举值Single、Reentrant和Multiple分别表示单线程、重入和多线程三种并发模式。关于并发和并发模式,将在本书的下一卷予以详细讲解,在这里就不再作重复介绍了。ConcurrencyMode在ServiceBehaviorAttribute的定义如下:

   1: [AttributeUsage(AttributeTargets.Class)]
   2: public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
   3: {
   4:     //其他成员
   5:     public ConcurrencyMode ConcurrencyMode { get; set; }   
   6: }
   1: public enum ConcurrencyMode
   2: {
   3:     Single,
   4:     Reentrant,
   5:     Multiple
   6: }

ConcurrencyMode.Single是默认采用的并发模式,这正是上面的例子中服务操作同步执行的根本原因。为了让服务操作异步地执行,从未提供服务的响应能力,我们只须要通过ServiceBehaviorAttribute将并发模式设为ConcurrencyMode.Multiple就可以了。

   1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
   2: public class CalculatorService : ICalculator, IDisposable
   3: {
   4:    //省略实现
   5: }

输出结果:

3/8/2009 08:05:05 : x + y = 3 when x = 1 and y = 2
3/8/2009 08:05:05 : x + y = 3 when x = 1 and y = 2
3/8/2009 08:05:05 : x + y = 3 when x = 1 and y = 2
3/8/2009 08:05:05 : x + y = 3 when x = 1 and y = 2
3/8/2009 08:05:06 : x + y = 3 when x = 1 and y = 2

如果将并发模式设为ConcurrencyMode.Multiple,意味着同一个服务实例在多个线程中被并发执行。当我们操作一些数据的时候,须要根据具体的情况考虑是否要采用一些加锁机制来确保状态的同步性。


作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
上一篇:CDN方式使用Vue组件通信


下一篇:ajax上传文件