由於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個請求來自相同的客戶端,它們是否還能夠及時地得到處理呢? 我們不妨通過我們的監控程序來說話。現在我們需要作的是修改客戶端進行服務調用的方式,讓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="http://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="http://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。毫無疑 問,在這種情況下最終表現出來的並發處理行為與會話類似。之所以只說類似,是因為單例模式下並沒有要求並發請求必須來自相同客戶 端的限制。