程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WCF技術剖析之二十三:服務實例(Service Instance)生命周期如何控制[下篇]

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

編輯:關於.NET

在[第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,意味著同一個服務實例在多個線程中被並發執行。當我們操作一些數據的時候,須要根據具體的情況考慮是否要采用一些加鎖機制來確保狀態的同步性。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved