WCF:併發處理

當多個線程同時訪問相同的資源的時候就會產生併發,WCF缺省狀況下會保護併發訪問。
對併發訪問須要恰當處理,控制很差不只會大大下降WCF服務的吞吐量和性能,並且還有可能會致使WCF服務的死鎖。
1、WCF併發模型:
在WCF中使用 ServiceBehaviorAttribute中的ConcurrencyMode屬性來控制這個設置。ConcurrencyMode屬性是個枚舉類型,有三個值:ConcurrencyMode.SingleConcurrencyMode.ReentrantConcurrencyMode.Multiple
public enum ConcurrencyMode
{
   Single,//默認,單線程模型
   Reentrant,//單線程,可重入模型,一般用在CallBack調用模型中
   Multiple//多線程模型
}
php

[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : ...
{
   public ConcurrencyMode ConcurrencyMode
   {get;set;}
   //More members
}
html

1.ConcurrencyMode.Single
單線程處理模式,同一個服務實例不會同時處理多個請求。當服務在處理請求時會對當前服務加鎖,若是再有其它請求須要該服務處理的時候,須要排隊等候。當服務處理完請求後會自動解鎖,隊列中的下個請求獲取服務資源,繼續處理。

例如:咱們去銀行去辦理業務,若是營業廳中只有一個窗口對外服務的話,那當前窗口每次只能處理一個用戶請求,若是再有其它用戶須要辦理業務的話,只能排隊等待。直到我辦理完業務後,營業窗口才能爲隊列中下個用戶提供服務。緩存

2.ConcurrencyMode.Reentrant
可重入的單線程處理模式,它仍然是單線程處理。服務端一次仍然只能處理一個請求,若是有多個請求同時到達仍然須要排隊。與單線程不一樣的是,請求在處理過程當中能夠去調用其它服務,等到其它服務處理完成後,再回到原服務等待隊列尾排隊。在調用其它服務的過程當中,會暫時釋放鎖,其它等待線程會趁機進行服務的調用。這種模式常見於服務端回調客戶端的場境中。
例如:咱們去銀行辦理業務,營業廳中仍是隻有一個窗口對外服務,一次只能處理一個用戶請求。我向銀行服務員請求辦理開戶業務,今後刻開始該服務被我鎖定,其它人員只能排隊等待。在服務員辦理業務的過程當中,會讓我填寫開戶申請表,這個簽字的過程就是服務端客戶端的回調過程。因爲填寫開戶申請表的時間會很長,爲了避免耽擱後面排隊顧客的時間,我暫時釋放對服務員的鎖定,到旁邊填寫申請表,讓後面的人員辦理業務。在我簽完字後再回到隊列中等待服務,當再輪到個人時候我再次鎖定服務,把憑據交給服務員,讓他繼續處理個人業務,這至關於服務的「重入」過程。 等到業務辦完後,我會離開銀行櫃檯,釋放對該服務的鎖定,等待隊列中的下我的員又開始鎖定服務辦理業務了。
服務器

3.ConcurrencyMode.Multiple
多線程模式,多線程模式能夠很好地增長系統的吞吐量。當多個用戶請求服務實例時,服務並不會加鎖,而是同時爲多個請求服務。這樣一來對全部用戶共享的資源就會產生影響,因此這種多線程的訪問模式須要對共享資源作好保護,大部份的狀況下需咱們的手動編寫代碼來實現多線程之間的訪問保護。
例如:咱們去吃烤肉串,烤肉串的師傅能夠同時爲多個顧客烤制,而不用一個一個地排隊等待,這就是典型的多線程處理模式。但這種模式若是不對肉串很好保護得的話那可麻煩了,比仿,我要的是3串麻辣味的,你要的是3串香辣味的,他要的是3串原味的,烤肉的時傅在烤制的時候須要對這三份肉串作好保護,防止作出一些「五味俱全」的肉串來。多線程

2、WCF中的併發模型與實例模型的關係。
實例模型與併發模型的關係很密切,對於不一樣的實例模型,併發所須要的併發管理也不同。併發

1.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single,InstanceContextMode = InstanceContextMode.PerCall) --Single併發與PerCall實例模型app

WCF足跡6:併發1 - Tony - Go ahead!

《圖1》
對於PerCall的實例模型,每一個客戶端請求都會與服務端的一個獨立的服務實例進行交互,就不會出現多個客戶端請求爭用一個服務實例的狀況,也就不會出現併發衝突。也就是說這時候ConcurrencyMode = ConcurrencyMode.Single寫與不寫都不要緊,不會影響吞吐量的問題。
Single併發是系統默認的,而PerCall實例模型也是系統默認的。也就是說在默認狀況下,WCF服務不會出現併發訪問衝突的狀況,固然對於靜態數據或全局緩存數據會產生併發衝突,後面會談到如何解決這個問題。
固然在進行回調操做的時候,有可能會出現等待超時的問題。這在下面4會談到。異步

服務端代碼:
    [ServiceContract]
    public interface ISinglePerCall
    {
        [OperationContract]
        int GetValue1();
        [OperationContract]
        int GetValue2();
    }
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall, ConcurrencyMode=ConcurrencyMode.Single)]
    public class SinglePerCall : ISinglePerCall
    {
        private int InstanceVariable = 0;
        private static int StaticVariable = 0;
        public int GetValue1()
        {
            return ++InstanceVariable;
        }
性能

        public int GetValue2()
        {
            return ++StaticVariable;
        }
    }
    這裏咱們定了服務契約ISinglePerCall,它裏面包含了兩個方法GetValue1()和GetValue2()。GetValue1()的主要做用是把實例變量的值加1返回;GetValue2()的主要做用是把靜態變量的值加1返回。
    服務的行爲模式是:Single併發+PerCall實例
    因爲PerCall實例模式會爲每一個請求生成一個新的服務實例,因此,咱們調用GetValue1()返回的結果應當永遠都是1。由於每一個服務實例在生成的時候都默認爲InstanceVariable設爲0;但調用GetValue2()的時候並不會都是1,由於GetValue2()讀取的是靜態變量自增後的值,靜態變量被全部實例所共享,因此它會從1開始遞增。因爲在這裏多個服務實例同時運行,對共享的靜態變量的訪問有能夠產生衝突,故會出現下面的運行結果。要解決這個問題還須要咱們手動編寫同步代碼。
    
客戶端代碼:
    class SinglePerCall
    {
        private static SRSinglePerCall.SinglePerCallClient client = new Client.SRSinglePerCall.SinglePerCallClient();
        public static void Main(string[] args)
        {
               
            for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();
                while (!thread.IsAlive) ;
            }
        }
        public static void DoWork()
        {
            
            Console.WriteLine("線程" + Thread.CurrentThread.GetHashCode() + ":/t實例變量的值:" +client.GetValue1());
            Console.WriteLine("線程" + Thread.CurrentThread.GetHashCode() + ":/t靜態變量的值:" +client.GetValue2());
        }
    }
    在客戶端中我啓用了10個線程向服務端發起調用,並顯示線程號、服務端實例變量的結果和靜態變量的結果。
        
運行結果:
ui

WCF足跡6:併發1 - Tony - Go ahead!

《圖1-1》
從圖中咱們看到實例變量的值都是1,靜態變量的值是出現遞增的情形,但因爲沒有采起同步策略,有可能會出現「線程9」和「線程11」這樣的併發衝突的狀況。


2.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single,InstanceContextMode = InstanceContextMode.PerSession) --Single併發與PerSession實例模型

WCF足跡6:併發1 - Tony - Go ahead!

《圖2》
對於PerSession實例模型,每一個客戶端對應一個服務實例,這樣在不一樣客戶端之間不會出現服務實例併發調用的狀況,但每一個客戶端能夠採用多線程調用同一個服務實例。因爲咱們採用了ConcurrencyMode.PerSession併發模式,這使得多線程在調用同一服務實例的時候會進行線程排隊,服務每次只對一個線程進行處理。
Single併發模式對多線程的客戶端的吞吐量會帶來影響,而對多個單線程的客戶端訪問並不起做用。
服務端代碼:
    [ServiceContract(SessionMode=SessionMode.Required)]
    public interface ISinglePerSessison
    {
        [OperationContract]
        int GetValue1();
        [OperationContract]
        int GetValue2();
    }
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Single)]
    public class SinglePerSessison : ISinglePerSessison
    {
        private int InstanceVariable = 0;
        private static int StaticVariable = 0;
        public int GetValue1()
        {
            return ++InstanceVariable;
        }

        public int GetValue2()
        {
            return ++StaticVariable;
        }
    }
    因爲使用的PerSession實例模式,因此每一個客戶端共享同一個服務實例,當同一客戶端使用多線程來訪問服務的話,會訪問同一個InstanceVariable變量。因爲是Single併發模式,因此不會對InstanceVariable變量產生併發衝突。
客戶端代碼:
    class SinglePerSession
    {
        private static SRSinglePerSession.SinglePerSessisonClient client = new Client.SRSinglePerSession.SinglePerSessisonClient();
        public static void Main(string[] args)
        {

           for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();

                while (!thread.IsAlive) ;
            }
        }
        public static void DoWork()
        {
            Console.WriteLine("線程" + Thread.CurrentThread.GetHashCode() + ":/t實例變量的值:" +client.GetValue1());
            Console.WriteLine("線程" + Thread.CurrentThread.GetHashCode() + ":/t靜態變量的值:" +client.GetValue2());
        }
    }
運行結果:

WCF足跡6:併發1 - Tony - Go ahead!

《圖2-1》
從圖中能夠看出,靜態變量依然是被不一樣客戶端不一樣的線程所共享;實例變量在同一個客戶端的多個線程中是共享的,在不一樣客戶端是各自獨立的。
在Single併發+PerSession實例中,實例變量不會產生併發衝突,而靜態變量可能會在不一樣的客戶端之間產生併發衝突,在同一客戶端多線程間不會產生併發衝突。

3.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single,InstanceContextMode = InstanceContextMode.Single) --Single併發與Single實例模型

WCF足跡6:併發1 - Tony - Go ahead!

《圖3》
對於Single實例模型,多個客戶端對應一個服務實例,這樣就容易發生多個客戶端同時請求一個服務進行處理,產生併發問題。當採起Single併發模式時,多個客戶端的併發訪問問題會獲得解決,由於Single併發模式會使多個客戶端請求進行排隊,依次處理每一個客戶端的請求。
Single併發模式在Single實例模型中,每次只能處理一個調用請求,會對系統的吞吐量帶來很大的影響

服務端代碼:
    [ServiceContract]
    public interface ISingleSingle
    {
        [OperationContract]
        int GetValue1();
        [OperationContract]
        int GetValue2();
    }
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
    public class SingleSingle : ISingleSingle
    {
        private int InstanceVariable = 0;
        private static int StaticVariable = 0;
        public int GetValue1()
        {
            return ++InstanceVariable;
        }

        public int GetValue2()
        {
            return ++StaticVariable;
        }
    }
客戶端代碼:
    class SingleSingle
    {
        private static SRSingleSingle.SingleSingleClient client = new Client.SRSingleSingle.SingleSingleClient();
        public static void Main(string[] args)
        {

            for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();

                while (!thread.IsAlive) ;
            }
        }
        public static void DoWork()
        {
            Console.WriteLine("線程" + Thread.CurrentThread.GetHashCode() + ":/t實例變量的值:" +client.GetValue1());
            Console.WriteLine("線程" + Thread.CurrentThread.GetHashCode() + ":/t靜態變量的值:" +client.GetValue2());
        }
    }
    
運行結果:

WCF足跡6:併發1 - Tony - Go ahead!

《圖3-1》
因爲咱們把實例模型設爲Single,那全部客戶端全部線程調用同一服務實例,故這些線程共享InstanceVariable實例變量,因爲併發模式是Single模式,因此不會對此變量產生併發衝突。而對於StaticVariable靜態變量,而言也是全部客戶端線程所共享的,但這裏的靜態變量StaticVariable不會產生併發衝突,由於只有一個服務實例來操做此靜態變量。

4.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant,InstanceContextMode = InstanceContextMode.PerCall) --Reentrant併發與PerCall實例模型

WCF足跡6:併發1 - Tony - Go ahead!

《圖4》
對於PerCall實例模型,若是採用Single的併發模式時,在實現回調的時候會出現問題:當服務在處理客戶請求的時候是處於鎖定狀態的,當回調的時候,在未解除鎖定的狀態下又去調用客戶端,在客戶端回調執行完成後,想再回到服務端的時候發現服務端被本身鎖定沒法進入,產生等待超時的異常,以下圖左側。
這就像在咱們參加認證考試同樣,考生進入考場打開電腦進行考試,至關於每一個考生獨佔一臺電腦。假設在考試過程當中某考生出去打了個電話,那當它再回到考場想繼續考試時,會被監考老師阻止,由於他違返了考場規則,沒法再使用那臺考試的電腦繼續考試。而該考生的那臺機器還被未交卷,仍被該考生佔用,沒法被他人使用。這樣就出現了「有去無回」狀況,即等待超時。
要解決這個問題,咱們能夠採用Reentrant併發模式,Reentrant模式依然是單線程模式,只是它容許客戶端加調用能再回到服務端繼續執行。
接上面的例子來講,就至關於考生在出去打電話的時候跟監考老師打個招乎,而後由巡考老師陪同一塊兒出去打電話,在回來的時候,由巡考老師把學員帶回考場交給監考老師繼續考試。這就實現了「有去有回」的可重入模式了。

示例設計思路:
客端使用多線程的方式連續向服務端發出三個調用請求。服務端使用PerCall實例模型和Reentrant併發模式,在ServiceMethod方法中實現對客戶端的回調操做。在客戶端回調代碼中只作了5秒的休眠時間。
爲了可以說明問題,在客戶端發出三個調用請求先後分別顯示一下時間;在服務端執行回調操做先後分別顯示一下時間。

服務端代碼:
    public interface IReentrantPerCallCallBack
    {
        [OperationContract]
        void ClientMethod();
    }
    [ServiceContract(CallbackContract = typeof(IReentrantPerCallCallBack))]
    public interface IReentrantPerCall
    {
        [OperationContract]
        void ServiceMethod();
    }
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.PerCall)]
    public class ReentrantPerCall : IReentrantPerCall
    {
        private IReentrantPerCallCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantPerCallCallBack>();
        public void ServiceMethod()
        {
            Debug.WriteLine("服務器端 " + this.GetHashCode() + ": 準備開始客戶端回調......" + DateTime.Now.ToString("hh:mm:ss ms"));
           callback.ClientMethod();
            Debug.WriteLine("服務器端 " + this.GetHashCode() + ":客戶端回調結束。" + DateTime.Now.ToString("hh:mm:ss ms"));

        }
    } 
客戶端代碼:
    class ReentrantPerCall:IReentrantPerCallCallback
    {
        private static ReentrantPerCall instance = new ReentrantPerCall();
        private static InstanceContext context = new InstanceContext(instance);
        private static SRReentrantPerCall.ReentrantPerCallClient client = new ReentrantPerCallClient(context);

        public void ClientMethod()
        {
            Thread.Sleep(5000);
        }
        public static void Main(string[] args)
        {
           for (int i = 0; i < 3; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();

            }
            
            Console.ReadLine();
            client.Close();
        }
        public static void DoWork()
        {
            Console.WriteLine("客戶端線程" + Thread.CurrentThread.GetHashCode() + "發起調用......" + DateTime.Now.ToString("hh:mm:ss ms"));
            client.ServiceMethod();
            Console.WriteLine("客戶端線程" + Thread.CurrentThread.GetHashCode() + "調用結束......" + DateTime.Now.ToString("hh:mm:ss ms"));

        }
    }
運行結果:

WCF足跡6:併發1 - Tony - Go ahead!

《圖4-1》
從圖中的結果咱們看出
客戶端:多線程發起調用時間是同樣的,即向服務端發出的請求幾乎是同一時間的。而線程結束的時間差異很大,之間幾乎相差5秒。
服務端:確實有三個服務實例響應客戶端的三個線程。
但從其發起調用和調用結束的時間來看,這三個服務實例並不是同時發起對客戶端的回調,而是有嚴格的前後次序。
按PerCall的實例模型來看,每一個線程在服務端都對應獨立的實例,這些服務都是並行處理的,既然客戶端發出的請求幾乎是同時的,那服務端就應當立馬根據線程的請求生成三個服務實例,實現對客戶端回調,而後重入服務端,最後結束調用。上面的代碼運行效果與理論分析明顯不一致。
這是爲「調用模型」產生的不一致性。
前面咱們談過,調用模型有三種:「請求-響應」(默認)、「One-Way」和「Duplex」。
這裏咱們沒有明確指出採用哪一種調用模式,所以爲默認使用「請求-響應」方式。而「請求-響應」模式,客戶端必須等服務端「響應」完成上一次「請求」後才能發出下一步「請求」。所以雖然客戶端使用多線程方式來調用服務,但最後的執行結果仍然表現出順序處理。要想使服務端可以並行處理客戶端請求的話,那咱們就不能使用「請求-響應」的調用模式,咱們可使用One-Way的方式來調用服務。
所以咱們能夠把服務端的ServiceMethod方法契約修正以下:
    [OperationContract(IsOneWay=true)]
    void ServiceMethod();

其他的代碼不作改變,雖然只是修了一下調用方式,但運行結果與以前並同樣。

WCF足跡6:併發1 - Tony - Go ahead!

《圖4-2》
經過運行結果咱們看到
客戶端:多線程的發起調用的時間是同樣的,結束調用的時間也是同樣的,而且發起調用和結束調用之間並無時間延遲,這說明客戶端多線程各自獨立地向服務端發出調用成功。
服務端:確實在服務端產生三個實例處理客戶端調用,而且這三個服務實例回調客戶端的時間是相同的,但客戶端回調結束的時間相差太大有明顯的5秒延遲。
根據上面的經驗,咱們能夠猜到這5秒的延遲是在服務端回調客戶端的過程當中引發的。回調過程實際上至關於客戶端與服務端換了個位置,由服務端來調用客戶端。因爲回調契約中的方法契約ClientMethod沒有明確設置調用方式,因此也是默認「請求-響應」的調用方式,若是回調沒有完成,則下個回調只能處於阻塞狀態。所以致使服務端的回調雖然同時發起,但結束回調的時間卻差異很大。
下面咱們在上面的基礎上再試着把回調契約中的ClientMethod方法契約聲明爲One-Way
    [OperationContract(IsOneWay=true)]
    void ClientMethod();

其他的代碼不作改變,運行結果:

WCF足跡6:併發1 - Tony - Go ahead!

《圖4-3》
從上圖咱們能夠看到
客戶端:多線程異步調用
服務端:多線程異步調用,中間再沒有發生延遲狀況。

話又說回來,這種回調契約IsOneWay=true調用的方式卻並非ConcurrencyMode = ConcurrencyMode.Reentrant的併發模式。
上面的例子,咱們是用一個代理的多個線程調用WCF服務,下面咱們把客戶端代碼修改一下,實現多個代理用各自的線程調用WCF服務。爲了能明確表達Reentrant併發模式,咱們服務端方法契約的IsOneWay=true去掉。
服務代碼:
    public interface IReentrantPerCallCallBack
    {
        [OperationContract]
        void ClientMethod();
    }
   [ServiceContract(CallbackContract = typeof(IReentrantPerCallCallBack))]
    public interface IReentrantPerCall
    {
        [OperationContract]
        void ServiceMethod();
    }
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.PerCall)]
    public class ReentrantPerCall : IReentrantPerCall
    {
        private IReentrantPerCallCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantPerCallCallBack>();
        public void ServiceMethod()
        {
            Debug.WriteLine("服務器端 " + this.GetHashCode() + ": 準備開始客戶端回調......" + DateTime.Now.ToString("hh:mm:ss ms"));
            callback.ClientMethod();
            Debug.WriteLine("服務器端 " + this.GetHashCode() + ":客戶端回調結束。" + DateTime.Now.ToString("hh:mm:ss ms"));

        }
    }
    服務端的方法契約仍然是「請求-響應」模式。
    
客戶代碼:
    class ReentrantPerCall:IReentrantPerCallCallback
    {
        public void ClientMethod()
        {
            Thread.Sleep(5000);
        }
        public static void Main(string[] args)
        {
            for (int i = 0; i < 3; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();

            }
            Console.ReadLine();
        }
        public static void DoWork()
        {
            ReentrantPerCall instance = new ReentrantPerCall();
            InstanceContext context = new InstanceContext(instance);
            SRReentrantPerCall.ReentrantPerCallClient client = new ReentrantPerCallClient(context);

           Console.WriteLine("客戶端線程" + Thread.CurrentThread.GetHashCode() + "發起調用......" + DateTime.Now.ToString("hh:mm:ss ms"));
            client.ServiceMethod();
            Console.WriteLine("客戶端線程" + Thread.CurrentThread.GetHashCode() + "調用結束......" + DateTime.Now.ToString("hh:mm:ss ms"));
            client.Close();

        }
    }
    代理類對象再也不是以成員變量的形式存在,而是在線程中被實例化,線程調用結束後會關閉代理類對象。這樣至關於多個客戶端代理同時向服務端發送請求。
    

運行結果:

WCF足跡6:併發1 - Tony - Go ahead!

《圖4-4》
從圖中能夠看出,在客戶端三個服務被同時發出,調用結束後幾乎同時返回。在服務端,也幾乎同時向客戶端發出回調,客戶端在通過5秒的延遲後也幾乎同時返回。休現了實例之間的無關。

(責任編輯:admin)

相關文章
相關標籤/搜索