在[第1篇]中,我們介紹了WCF關於實例管理一些基本的知識點,包括InstanceContext、InstanceContextMode、已經如何通過ServiceBehaviorAttribute應用不同的實例上下文模式給不同的服務。在[第1篇]中,對WCF采用的三種不同實例上下文模式進行了簡單的比較,本篇的重點方法對單調(PerCall)模式為進行詳細介紹。
在單調(Per-Call)實例上下文模式下,WCF總是創建一個新的服務實例上下文處理接收到的每一個服務調用請求,並在服務操作執行結束後,回收服務上下文和服務實例。換句話說,單調服務實例上下文模式使服務實例上下文的生命周期與服務調用本身綁定。我們首先來介紹單調模式下服務實例上下文具體有怎樣的生命周期。
一、 單調模式下的服務實例上下文提供機制
對於單調模式,服務實例的生命周期大體上可以看成服務操作執行的生命周期。服務實例在服務操作執行前被創建,在操作完成之後被回收。下面的列表揭示了在單調模式下,對於每一次服務調用請求,WCF的整個服務實例激活過程:
WCF服務端接收到來自客戶端的服務調用請求;
通過實例上下文提供者(InstanceContextProvider)對象試圖獲取現有服務實例的實例上下文,對於單調模式,返回的實例上下文永遠為空;
如果獲取實例上下文為空,則通過實例提供者(IntanceProvider)創建服務實例,封裝到新創建的實例上下文中;
通過InstanceContext的GetServiceInstance方法獲取服務實例對象,借助操作選擇器(OperationSelector)選擇出相應的服務操作,最後通過操作執行器(OperationInvoker)對象執行相應的操作方法;
操作方法執行完畢後,關閉被卸載InstanceContext對象。在此過程中,會調用InstanceProvider對象釋放服務實例,如果服務類型實現了接口IDisposable,則會調用Disposable方法;
服務實例成為垃圾對象,等待GC回收。
對於上述列表中提到的InstanceContextProvider、InstanceProvider等重要的對象,以及相關的實現機制,將在本系列後續的部分進行單獨講解。為了加深讀者的理解,這裡通過一個簡單的例子來演示在單調模式下服務實例的整個激活流程。
二、 實例演示:單調模式下服務實例的生命周期
本案例依然沿用典型的4層結構和計算服務的場景,下面是服務契約和具體服務實現的定義。在CalculatorService類型上,通過ServiceBehaviorAttribute特性將實例上下文模式設為單調(Per-Call)模式。為了演示服務實例的創建、釋放和回收,我們分別定義了無參構造函數,終止化器(Finalizer)以及實現的接口IDisposable,並在所有的方法中輸出相應的指示性文字,以便更容易地觀測到它們執行的先後順序。
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: double Add(double x, double y);
9: }
10: }
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Contracts;
4: namespace Artech.WcfServices.Services
5: {
6: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
7: public class CalculatorService : ICalculator, IDisposable
8: {
9: public CalculatorService()
10: {
11: Console.WriteLine("Service object is instantiated.");
12: }
13: ~CalculatorService()
14: {
15: Console.WriteLine("Service object is finalized.");
16: }
17:
18: public void Dispose()
19: {
20: Console.WriteLine("Service object is disposed.");
21: }
22: public double Add(double x, double y)
23: {
24: Console.WriteLine("Operation method is invoked.");
25: return x + y;
26: }
27: }
28: }
為了演示GC對服務實例的回收,在進行服務寄宿的時候,通過System.Threading.Timer使GC每隔10毫秒強制執行一次垃圾回收。
1: using System;
2: using System.ServiceModel;
3: using System.Threading;
4: using Artech.WcfServices.Services;
5: namespace Artech.WcfServices.Hosting
6: {
7: public class Program
8: {
9: private static Timer GCScheduler;
10:
11: static void Main(string[] args)
12: {
13: GCScheduler = new Timer(
14: delegate
15: {
16: GC.Collect();
17: }, null, 0, 100);
18: using (ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService)))
19: {
20: serviceHost.Open();
21: Console.Read();
22: }
23: }
24: }
25: }
通過一個控制台應用程序對服務進行成功寄宿後,客戶端通過下面的代碼,使用相同的服務代理對象進行兩次服務調用。
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Contracts;
4: namespace Artech.WcfServices.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
11: {
12: ICalculator calculator = channelFactory.CreateChannel();
13: Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2));
14: Console.WriteLine("x + y = {2} when x = {0} and y = {1}: {3}", 1, 2, calculator.Add(1, 2));
15: }
16: }
17: }
18: }
從運行後服務端的輸出可以看出,對於兩次服務調用請求,服務端先後創建了兩個服務實例,在操作方法成功執行後,Dispose方法得以執行。而終止化器(Finalizer)是被GC在後台執行的,所以執行的時機不能確定。不過有一點可以從中得到證實:當服務操作執行時,服務實例變成了“垃圾”對象,並可以被GC回收以騰出占據的內存空間。
Service object is instantiated.
Operation method is invoked.
Service object is disposed.
Service object is instantiated.
Operation method is invoked.
Service object is disposed.
Service object is finalized.
Service object is finalized.
三、 服務實例上下文的釋放
如果服務實例須要引用一些非托管資源,比如數據庫連接、文件句柄等,須要及時將其釋放。在這種情況下,我們可以通過實現IDisposable接口,在Dispose方法中進行相應的資源回收工作。在單調實例上下文模式下,當服務操作執行時,Dispose方法會自動被執行,這一點已經通過上面的案例演示得到證實。
對於實現了IDisposable接口的Dispose方法,有一點值得注意的是:該方法是以與操作方法同步形式執行的。也就是說,服務操作和Dispose方法在相同的線程中執行。認識這一點很重要,因為無論采用怎樣的實例模式,在支持會話(Session)的情況下如果服務請求來自於同一個服務代理,服務操作都會在一個線程下執行。對於單調模式就會出現這樣的問題:由於Dispose方法同步執行的特性,如果該方法是一個比較耗時的操作,那麼來自於同一個服務代理的服務後續調用請求將不能得到及時執行。WCF只能在上一個服務實例被成功釋放之後,才能處理來自相同服務代理的下一個服務調用請求。為了讓讀者體會到同步方式釋放服務實例在應用中的影響,並證明同步釋放服務實例的現象,我們對上面的案例略加改動。
在CalculatorService中,通過線程休眠的方式模擬耗時的服務實例釋放操作(5秒)。在Dispose和Add方法中,除了輸出具體操作名稱之外,還會輸出當前的線程ID和執行的開始時間,代碼如下所示。
1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
2: public class CalculatorService : ICalculator, IDisposable
3: {
4: public void Dispose()
5: {
6: Console.WriteLine("Time: {0}; Thread ID: {1}; Service object is disposed.", DateTime.Now, Thread.CurrentThread.ManagedThreadId);
7: Thread.Sleep(5000);
8: }
9: public double Add(double x, double y)
10: {
11: Console.WriteLine("Time: {0}; Thread ID: {1}; Operation method is invoked.", DateTime.Now, Thread.CurrentThread.ManagedThreadId);
12: return x + y;
13: }
14: }
在客戶端,我們創建兩個不同的服務代理,通過ThreadPool分別對它們進行2次異步調用。下面是相關的服務調用代碼。
1: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
2: {
3: ICalculator calculator = channelFactory.CreateChannel();
4: ThreadPool.QueueUserWorkItem(delegate
5: {
6: Console.WriteLine("{3}: x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2), DateTime.Now);
7: });
8: ThreadPool.QueueUserWorkItem(delegate
9: {
10: Console.WriteLine("{3}: x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2), DateTime.Now);
11: });
12: Console.Read();
13: }
從客戶端和服務端輸出結果的比較,我們可以清晰地看出基於相同服務代理的操作方法和Dispose方法都執行在相同的線程下(線程ID為12),並且兩次服務操作的間隔為服務實例釋放的時間:5秒。由於服務操作和Dispose方法的同步執行,導致服務端忙於釋放上一個服務實例,而不能及時處理來自相同服務代理的下一個服務調用請求。
客戶端:
3/6/2009 7:12:34 PM: x + y = 3 when x = 1 and y = 2
3/6/2009 7:12:39 PM: x + y = 3 when x = 1 and y = 2
服務端:
Time: 3/6/2009 7:12:34 PM; Thread ID: 12; Operation method is invoked.
Time: 3/6/2009 7:12:34 PM; Thread ID: 12; Service object is disposed.
Time: 3/6/2009 7:12:39 PM; Thread ID: 12; Operation method is invoked.
Time: 3/6/2009 7:12:39 PM; Thread ID: 12; Service object is disposed.
關於服務實例的同步執行機制,還有一點需要說明是,在Dispose方法中,可以得到當前OperationContext,而OperationContext在會話(Per-Session)實例上下文模式下是不可得的。
四、單調模式與可擴展性
在單調模式下,如果不考慮GC對垃圾對象回收的滯後性,服務實例的數量可以看成是當前正在處理的服務調用請求的數量。相關的資源能夠在服務操作執行完畢之後得到及時回收(通過實現IDisposable接口,將資源回收操作實現在Dispose方法中)。所以,單調模式具有的優勢是能夠最大限度地發揮資源的利用效率,避免了資源的閒置和相互爭用。
這裡的資源不僅僅包括服務實例本事占據的內存資源,也包括服務實例直接或間接引用的資源。由於單調模式采用基於服務調用的服務實例激活和資源分配方式,所以服務實例或被分配的資源自始至終都處於“工作”狀態,不會造成資源的閒置。服務實例在完成其使命之後,能夠對資源進行及時的釋放,被釋放的資源可以及時用於對其他服務請求的處理。
我們將單調模式和後面要講的會話模式作一個對比,後者采用基於服務代理的實例激活和生命周期管理。也就是說,在不考慮WCF閒置請求策略(當服務實例在超出某個時間段沒有被使用的情況下,WCF將其清理)的情況下,服務實例的生命始於通過服務實例進行第一次服務調用,或者調用Open方法開啟服務代理之時,服務代理的關閉會通知WCF服務端框架將對應的服務實例進行釋放。舉一個極端的例子,服務實例在存續期間需要引用一個非托管資源,比如是數據庫連接,假設最大允許的並發連接為100。現在,先後100個客戶端(或者服務代理)進行服務調用請求,毫無疑問,100個服務實例會被創建並同時存在於服務端的內存之中,並且每一個服務實例引用一個開啟狀態的數據庫連接,那麼當來自第101個客戶端服務調用請求抵達時,將得不到處理,除非在它的超時時限到達之前,有一個客戶端自動將服務代理關閉。
但是,對於相同的場景,如果采用單調的模式,就能應付自如,因為在每次服務調用之後,數據庫的連接可以及時地得到關閉和釋放。
對於單調模式,很多讀者一開始就會心存這樣的疑問:服務實例的頻繁創建,對性能不會造成影響嗎?在前一章中,我們就說過:高性能(Performance)和高可擴展性(Scalability)是軟件設計與架構中永遠不可以同時兼顧的,原因很簡單,高性能往往需要充足的資源,高擴展性又需要盡可能地節約資源。所以我們才說,軟件設計與架構是一項“權衡”的藝術,我們的目的不是將各個方面都達到最優,因為這是不可能實現的任務,我們須要做的只是找到一個平衡點使整體最優。關於高擴展性和性能之間的平衡關系,我們很難有一個適合所有場景的黃金法則,這需要對具體場景的具體分析。
較之會話模式,單調模式能夠處理更多的並發客戶端,提供更好的吞吐量(Throughput)。對於量化我們的服務到底能夠處理多少客戶端,Juval Lowy在其著作《Programming WCF》中提出了這樣一項經驗性總結:在一個典型的企業應用中,並發量大概是所有客戶端數量的1%(高並發情況下能達到3%),也就是如果服務端能夠同時維持100個服務實例,那麼意味著能為10 000個客戶端提供服務。
關於服務實例的創建過程,其中會使用到諸如反射這樣的相對影響性能的操作,但是在WCF應用中,真正影響性能是操作時信道的創建和釋放。服務實例的激活和它們比起來,可以說是微不足道。但是,如果在應用中出現對基於相同服務代理的頻繁調用,比如服務調用放在一個For循環中調用上百次,服務實例的創建帶來的性能損失就不能不考慮了。