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
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-11-03 17:47:37

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

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

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

WCF技术剖析之二十七: 如何将一个服务发布成WSDL

[基于WS-MEX的实现](提供模拟程序) 通过<如何将一个服务发布成WSDL[编程篇]>的介绍我们知道了如何可以通过编程或者配置的方式将ServiceMetadataBehavior这样一个服务形式应用到相应的服务上面,从而实现基于HTTP-GET或者WS-MEX的元数据发布机制.那么在WCF内部具体的实现原理又是怎样的呢?相信很多人对此都心存好奇,本篇文章的内容将围绕着这个主题展开. 一. 从WCF分发体系谈起 如果读者想对WCF内部的元数据发布机制的实现原理有一个全面而深入的了解,必须对

WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于WS-MEX的实现](提供模拟程序)

通过<如何将一个服务发布成WSDL[编程篇]>的介绍我们知道了如何可以通过编程或者配置的方式将ServiceMetadataBehavior这样一个服务形式应用到相应的服务上面,从而实现基于HTTP-GET或者WS-MEX的元数据发布机制.那么在WCF内部具体的实现原理又是怎样的呢?相信很多人对此都心存好奇,本篇文章的内容将围绕着这个主题展开. 一. 从WCF分发体系谈起 如果读者想对WCF内部的元数据发布机制的实现原理有一个全面而深入的了解,必须对WCF服务端的分发体系有一个清晰的认识.在这

WCF技术剖析之二十六:如何导出WCF服务的元数据(Metadata)[扩展篇]

通过<实现篇>对WSDL元素和终结点三要素的之间的匹配关系的介绍,我们知道了WSDL的Binding元素来源于终结点的绑定对象,那么这些基于Binding的元数据以及相应的策略断言是如何被写入WSDL的呢?WSDL导出扩展(WSDL Export Extension)和策略导出扩展(Policy Export Extension)就是为此设计的. 一.WSDL导出扩展(WSDL Export Extension) 终结点的绑定本质上就是相关的绑定元素(BindingElement)的有序组合(

WCF技术剖析之二十一:WCF基本异常处理模式[下篇]

从FaultContractAttribute的定义我们可以看出,该特性可以在同一个目标对象上面多次应用(AllowMultiple = true).这也很好理解:对于同一个服务操作,可能具有不同的异常场景,在不同的情况下,需要抛出不同的异常. 1: [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] 2: public sealed class FaultContractAttri

WCF技术剖析之二十五:元数据(Metadata)架构体系全景展现[WS标准篇]

元数据实际上是服务终结点的描述,终结点由地址(Address).绑定(Binding)和契约(Contract)经典的ABC三要素组成.认真阅读过<WCF技术剖析(卷1)>的读者相对会对这三要素的本质有一个深刻的认识:地址决定了服务的位置并实现相应的寻址机制:契约描述了消息交换模式(Message Exchange Pattern: MEP)以及消息的结构(Schema):绑定则通过创建信道栈实现对消息的编码.传输和基于某些特殊的功能(比如实现事务.可靠传输以及基于消息的安全)对消息作出的处理

WCF技术剖析之二十二: 深入剖析WCF底层异常处理框架实现原理[下篇]

WCF客户端和服务端的框架体系相互协作,使得开发人员可以按照我们熟悉的方式进行异常的处理:在服务操作执行过程中抛出异常(FaultException),在调用服务时捕获异常,完全感觉不到"分布式"的存在,如同典型的"本地"操作一般.为了实现这样的效果,WCF在内部为我们作了很多. 消息交换是WCF进行通信的唯一手段,消息不仅仅是正常服务调用请求和回复的载体,服务端抛出的异常,甚至是服务的元数据都是通过消息的形式传向客户端的.所以,实现异常与消息之间的转换是整个异常处

WCF技术剖析之九:服务代理不能得到及时关闭会有什么后果?

我们想对WCF具有一定了解的人都会知道:在客户端通过服务调用进行服务调用过程中,服务代理应该及时关闭.但是如果服务的代理不等得到及时的关闭,到底具有怎样的后果?什么要关闭服务代理?在任何时候都需要关闭服务代理吗?是否有一些例外呢?本篇文章将会围绕着这些问题展开. 一.会话信道(Sessionful Channel) V.S. 数据报信道(Datagram Channel) WCF通过信道栈实现了消息的编码.传输及基于某些特殊功能对消息的特殊处理,而绑定对象是信道栈的缔造者,不同的绑定类型创建出来

WCF技术剖析之二十四:ServiceDebugBehavior服务行为是如何实现异常的传播的?

服务端只有抛出FaultException异常才能被正常地序列化成Fault消息,并实现向客户端传播.对于一般的异常(比如执行Divide操作抛出的DivideByZeroException),在默认的情况下,异常信息无法实现向客户端传递.但是,倘若为某个服务应用了ServiceDebugBehavior这么一个服务行为,并开启了IncludeExceptionDetailInFaults开关,异常信息将会原封不动地传播到客户端.WCF内部是如何处理抛出的非FaultException异常的呢?