並發與實例上下文模式: WCF服務在不同實例上下文模式下具有怎樣的並發表現
由於WCF的並發是針對某個封裝了服務實例的InstanceContext而言的,所以在不同的實例上下文模式下,會表現出不同的並發行為。接下來,我們從具體的實例上下文模式的角度來剖析WCF的並發,如果對WCF實例上下文模式和實例上下文提供機製不了解的話,請參閱《WCF技術剖析(卷1)》第9章。
在《實踐重於理論》一文中,我寫一個了簡單的WCF應用,通過這個應用我們可以很清楚了監控客戶端和服務操作的執行情況下。借此,我們可以和直觀地看到服務端對於並發的服務調用請求,到底采用的是並行還是串行的執行方式。接下來,我們將充分地利用這個監控程序,以實例演示加原理分析相結合的方式對不同實例上下文模式下的並發實現機製進行深度剖析。
一、單調(PerCall)實例上下文模式
由於WCF的並發是針對某個封裝了服務實例的InstanceContext而言的,但是對單調的實例上下文模式,WCF服務端運行時總是創建一個全新的InstanceContext來處理每一個請求,不管該請求是否來自相同的客戶端。所以在單調實例上下文模式下,根本就不存在對某個InstanceContext的並發調用的情況發生。
我們可以通過我們監控程序來驗證這一點。為此,我們需要通過ServiceBehaviorAttribute將實例上下文模式設置成InstanceContextMode.PerCall,相關的代碼如下所示。
1: [ServiceBehavior(UseSynchronizationContext = false,InstanceContextMode = InstanceContextMode.PerCall)]
2: public class CalculatorService : ICalculator
3: {
4: //省略成員
5: }
下麵是客戶端進行並發服務調用的代碼:
1: for (int i = 1; i <= 5; i++)
2: {
3: ThreadPool.QueueUserWorkItem(state =>
4: {
5: int clientId = Interlocked.Increment(ref clientIdIndex);
6: ICalculator proxy = _channelFactory.CreateChannel();
7: using (proxy as IDisposable)
8: {
9: EventMonitor.Send(clientId, EventType.StartCall);
10: using (OperationContextScope contextScope = new OperationContextScope(proxy as IContextChannel))
11: {
12: MessageHeader<int> messageHeader = new MessageHeader<int>(clientId);
13: OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader.GetUntypedHeader(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace));
14: proxy.Add(1, 2);
15: }
16: EventMonitor.Send(clientId, EventType.EndCall);
17: }
18: }, null);
19: }
如果在此基礎上運行我們的監控程序,將會得到如圖1所示的輸出結果,從中我們可以看出,仍然我們采用默認的並發模式(ConcurrencyMode.Single),來自5個不同客戶端(服務代理)的調用請求能夠及時地得到處理。
圖1 單調實例上下文模式下的並發事件監控輸出(不同客戶端)
上麵我們演示了WCF服務端處理來自不同客戶端並發請求的處理,,它們是否還能夠及時地得到處理呢?我們不妨通過我們的監控程序來說話。現在我們需要作的是修改客戶端進行服務調用的方式,讓5個並發的調用來自於相同的服務代理對象,相關的代碼如下所示。為了便於跟蹤,我們依然將並發的序號1~5通過消息報頭傳遞到服務端。不過在這裏它不代表客戶端,而是代表某個服務調用而已。
1: ICalculator proxy = _channelFactory.CreateChannel();
2: for (int i = 1; i < 6; i++)
3: {
4: ThreadPool.QueueUserWorkItem(state =>
5: {
6: int clientId = Interlocked.Increment(ref clientIdIndex);
7: EventMonitor.Send(clientId, EventType.StartCall);
8: using (OperationContextScope contextScope = new OperationContextScope(proxy as IContextChannel))
9: {
10: MessageHeader<int> messageHeader = new MessageHeader<int>(clientId);
11: OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader.GetUntypedHeader(EventMonitor.CientIdHeaderLocalName, EventMonitor.CientIdHeaderNamespace));
12: proxy.Add(1, 2);
13: }
14: EventMonitor.Send(clientId, EventType.EndCall);
15: }, null);
16: }
再次運行我們的監控程序,你將會得到完全不一樣的輸出結果(如圖2所示)。從監控信息我們可以很清晰地看出,服務操作的執行完全是以串行化的形式執行的。對於服務端來說,似乎仍然是以同步的方式方式處理並發的服務調用請求的。但是我們說過,WCF並發機製的同步機製是通過對InstanceContext進行加鎖實現的。但是對於單調實例上下文模式來說,雖然5個請求來自相同的客戶端,但是對應的InstanceContext卻是不同的。難道我們前麵的結論都是錯誤的嗎?
圖2 單調實例上下文模式下的並發事件監控輸出(相同客戶端)
實際上出現如圖2所示的監控輸出與WCF並發框架體係采用的同步機製一點關係都沒有。在說明原因之前,我們先來給出解決方案。我們隻需要在進行服務調用之前,調用Open方法顯式地開啟服務代理,你就會得到與圖4-5類似的輸出結果,相應的代碼如下所示:
1: ICalculator proxy = _channelFactory.CreateChannel();
2: (proxy as ICommunicationObject).Open();
3: for (int i = 1; i < 6; i++)
4: {
5: //省略其他代碼
6: }
上麵的問題涉及到WCF一個很隱晦的機製,相信不會有太多人知道它的存在。如果我們直接通過創建出來的服務代理對象(並沒有顯示開啟服務代理)進行服務調用,WCF客戶端框架會通過相應的機製確保服務代理的開啟,我們可以將這種機製成為服務代理自動開啟。在內部,WCF實際上是將本次調用放入一個隊列之中,等待上一個放入隊列的調用結束。也就是說,針對一個沒有被顯式開啟的服務代理的並發調用實際上是以同步或者串行的方式執行的。
但是,如果你在進行服務調用之前通過我們上麵代碼的方式顯式地開啟服務代理,基於該代理的服務調用就能得到機製處理。所以,當你真的需要執行基於相同服務代理的並發調用的時候,請務必對服務代理進行顯式開啟。
並發的問題挺多,到這裏還沒完。現在我們保留上麵修改過的代碼(確保在進行並發服務調用之前顯示開啟服務代理),將客戶端和服務終結點采用的綁定類型從WS2007HttpBinding換成NetTcpBinding或者NetNamedPipeBinding。在此運行我們的監控程序,你又將得到類似於如圖2所示的監控信息。也就是說,如果采用向NetTcpBinding或者NetNamedPipeBinding這種天生就支持會話的綁定類型(因為它們基於的傳輸協議提供了對會話的原生支持,HTTP協議本身是沒有會話的概念的),對於基於單個服務代理的同步調用,最終表現出來仍就是串行化執行。這是WCF信道架構體係設計使然,我個人對這個設計不以為然。
二、 會話(PerSession)實例上下文模式和單例實例(Single)上下文模式
在基於會話的實例上下文提供機製下,被創建出來封裝服務實例的InstanceContext與會話(客戶端或者服務代理)綁定在一起。也就是說,InstanceContext和服務代理是具有一一對應的關係。基於我們前麵介紹的基於對InstanceContext加鎖的同步機製,如果服務端接收到的並發調用是基於不同的客戶端,那麼它們會被分發給不同的InstanceContext,所以對於它們的處理是並行的。因此,我們主要探討的是針對相同客戶端的並發調用的問題。
在《WCF技術剖析(卷1)》的第9章中,我們對WCF的會話進行過深入的剖析。如果讀者對其中的內容還熟悉的話,一定知道WCF的會話最終取決於以下三個方麵的因素:
- 服務契約采用SessionMode.Allowed或者SessionMode.Required的會話模式;
- 服務采用InstanceContextMode.PerSession的實例上下文模式;
- 終結點的綁定提供對會話的支持。
所以說,即使我們通過ServiceBehaviorAttribute特性將服務的實例上下文模式設置成InstanceContextMode.PerSession,如果不滿足其餘兩個條件,WCF仍然采用的是基於單調的實例上下文提供機製,那麼表現出來的並發處理行為就與單調模式別無二致了。
我們依然可以通過我們的監控程序來證實這一點,現在我們在CalculatorService類型上應用ServiceBehaviorAttribute特性將實例上下文模式設置成InstanceContextMode.PerSession。
1: [ServiceBehavior(UseSynchronizationContext = false,InstanceContextMode = InstanceContextMode.PerSession)]
2: public class CalculatorService : ICalculator
3: {
4: //省略成員
5: }
然後我們破壞第一個條件,通過ServiceContractAttribute特性將服務契約ICalculator的會話模式設置成SessionMode.NotAllowed。
1: [ServiceContract(Namespace="https://www.artech.com/",SessionMode = SessionMode.NotAllowed)]
2: public interface ICalculator
3: {
4: //省略成員
5: }
我們也可以破環第三個條件,讓終結點綁定不支持會話。無論對WSHttpBinding還是WS2007HttpBinding,隻有在支持某種安全模式或者可靠會話(Reliable Sessions)的情況下,它們才提供對會話的支持。由於WS2007HttpBinding默認采用基於消息的安全模式,如果我們將安全模式設置成None,綁定將不再支持會話。為此,我們對服務端的配置進行了如下的修改,當然客戶端必須進行相應地修改,在這裏就不再重複介紹了。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <bindings>
5: <ws2007HttpBinding>
6: <binding name="nonSessionBinding">
7: <security mode="None"/>
8: </binding>
9: </ws2007HttpBinding>
10: </bindings>
11: <services>
12: <service name="Artech.ConcurrentServiceInvocation.Service.CalculatorService">
13: <endpoint bindingConfiguration="nonSessionBinding" address="https://127.0.0.1:3721/calculatorservice" binding="ws2007HttpBinding" contract="Artech.ConcurrentServiceInvocation.Service.Interface.ICalculator" />
14: </service>
15: </services>
16: </system.serviceModel>
17: </configuration>
當我們進行了如此修改後再次運行我們的監控程序,你可以得到類似於如圖1所示的表現為並行化處理的監控結果。
如果同時滿足上述的三個條件,來自於相同客戶端的並發請求是分發到相同的InstanceContext。在這種情況下,WCF將按照相應並發模式語義上體現的行為來處理這些並發的請求。ConcurrencyMode.Single和ConcurrencyMode.Multiple體現的分別是串行化和並行化的處理方式,如果ConcurrencyMode.Reentrant,則後續的請求隻有在前一個請求處理結束或者對外調用(Call Out)的時候才有機會被處理。
對於采用單例實例上下文模式,所有的服務調用請求,不論它來自於那個客戶端,最終都會被分發給同一個InstanceContext。毫無疑問,在這種情況下最終表現出來的並發處理行為與會話類似。之所以隻說類似,是因為單例模式下並沒有要求並發請求必須來自相同客戶端的限製。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-27 15:04:53