WCF足跡7:併發2

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



《圖5》
對於PerSession的實例模型,每個客戶端擁有一個服務實例,如果該客戶端採用多線程模式與服務端交互,那這個客戶端的所有線程共享一個實例。而Reentrant併發模式,依然是個單線程的模式,即某時刻只能有一個線程調用服務實例,其餘的線程處於等待隊列中。但是當某線程回調客戶端操作時,該線程就不再鎖定該服務實例,等待隊列中的下個線程將鎖定服務實例進行調用。當回調客戶端的線程執行完客戶端調用後再回到服務端時,它將進入線程等待隊列中重新排隊等待。
在“Reentrant併發+PerSession實例”模式中關於“調用方式(請求-響應、OneWay和Duplex)”對運行結果的影響,與上面“Reentrant併發+PerCall實例”模式中的“調用方式”對結果的影響是一樣的。在這裏我們不再逐一演示,大家可以跟據上面的講述自己編寫代碼來驗證。
下面我們依然把服務契約中的方法契約調用設置爲IsOneWay=true。代碼如下:
服務端代碼:
    public interface IReentrantPerSessionCallBack
    {
        [OperationContract]
        void ClientMethod();
    }
    [ServiceContract(CallbackContract = typeof(IReentrantPerSessionCallBack), SessionMode = SessionMode.Required)]
    public interface IReentrantPerSession
    {
        [OperationContract(IsOneWay=true)]
        void ServiceMethod();
    }
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.PerSession)]
    public class ReentrantPerSession : IReentrantPerSession
    {
        private IReentrantPerSessionCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantPerSessionCallBack>();
        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"));
        }
    }
    在服務端代碼中,我們沒有把回調方法契約設爲IsOneWay=true。
客戶端代碼:
    public class ReentrantPerSession:SRReentrantPerSession.IReentrantPerSessionCallback
    {
        private static ReentrantPerSession instance = new ReentrantPerSession();
        private static InstanceContext context = new InstanceContext(instance);
        private static SRReentrantPerSession.ReentrantPerSessionClient client = new Client.SRReentrantPerSession.ReentrantPerSessionClient(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() + "發起調用......");
            client.ServiceMethod();
            Console.WriteLine("客戶端線程" + Thread.CurrentThread.GetHashCode() + "調用結束....../n");
        }
    }
客戶端中在Main函數中,依然使用同一個服務代理對象,採用多線程向服務端發出調用。在回調方法中先休眠5秒後再“重入”服務端。
   
運行效果:



《圖5-1》
從圖中我們看到
客戶端中我們使用同一個代理對象的多線程技術向服務端發送請求,在這裏我啓用了兩個客戶端。
而服務器端一共產生了兩個實例,每個實例代表一個客戶端的調用。
在每個服務對象中,我們可以看到三個客戶端回調的時間基本相同,而客戶端回調返回的時間卻相隔5秒鐘。
關於三個回調時間基本相同,並不代表服務端使用多線程同時對客戶端進行三次回調,Reentrant併發模式仍然是單線程模式。原因是,當客戶端三次請求(A,B,C)到達服務端後,A請求將鎖定服務實例,並調用服務,B請求和C請求將在等待隊列中排隊。在A請求處理過程中進行回調時,A請求會暫時釋放對服務實例的鎖定,此時隊列中的B請求會鎖定服務,進行調用。當B請求處理回調時,B請求也會暫時釋入服務實例鎖定,此時C請求會鎖定服務進行調用......。當A回調結束後會重入服務實例,並在等待隊列的尾部排隊等待後續處理。
關於客戶端返回時間相隔5秒種,原因我們上面已經說過,即回調契約是“請求-響應”模式,只有收到上次調用的“響應”才能進行下次“請求”。

同樣我們再把代碼修改一下,使用多個代理用各自的線程調用WCF服務,來模擬多用戶來調用服務實例。
服務端代碼:
    public interface IReentrantPerSessionCallBack
    {
        [OperationContract]
        void ClientMethod();
    }
    [ServiceContract(CallbackContract = typeof(IReentrantPerSessionCallBack), SessionMode = SessionMode.Required)]
    public interface IReentrantPerSession
    {
       //[OperationContract(IsOneWay=true)]
        [OperationContract]
        void ServiceMethod();
    }
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.PerSession)]
    public class ReentrantPerSession : IReentrantPerSession
    {
       private IReentrantPerSessionCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantPerSessionCallBack>();
        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"));
        }
    }
    在服務端代碼中,ServiceMethod方法契約仍然使用“請求響應”的調用模式。
客戶端代碼:
    public class ReentrantPerSession:SRReentrantPerSession.IReentrantPerSessionCallback
    {
        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()
        {
            ReentrantPerSession instance = new ReentrantPerSession();
            InstanceContext context = new InstanceContext(instance);
            SRReentrantPerSession.ReentrantPerSessionClient client = new Client.SRReentrantPerSession.ReentrantPerSessionClient(context);

            Console.WriteLine("客戶端線程" + Thread.CurrentThread.GetHashCode() + "發起調用......");
           client.ServiceMethod();
            Console.WriteLine("客戶端線程" + Thread.CurrentThread.GetHashCode() + "調用結束....../n");
            client.Close();
        }
    }
    在客戶端,在線程內部使用不同的代理向服務端發起調用。
運行效果:



《圖5-2》
從圖中我們可以看出來,每個客戶端只很服務端發起一個線程的調用,這種調用模式能夠實現對服務端的並行調用,並且也不會產生併發衝突,其實這種用法相當於“Reentrant併發模式+PerCall實例模型”。


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



《圖6》
對於“Single實例模型+Reentrant併發模式”與“PerSession實例模型+Reentrant併發模式”有些相似,是單線程重入處理模式。如果調用方式是“請求響應”方式時,如果是同一個代理的多個線程會受到阻塞,如果是多個代理則不同代理會並行調用;如果是OneWay調用模式,則不會出現任阻塞情況。
上面“Single實例模型+Reentrant併發模式”與“PerSession實例模型+Reentrant併發模式”這兩個模型就與“去銀行排隊”的例子很相似。

服務端代碼:
    public interface IReentrantSingleCallBack
    {
        [OperationContract]
        void ClientMethod();
    }
    [ServiceContract(CallbackContract = typeof(IReentrantSingleCallBack))]
    public interface IReentrantSingle
    {
        [OperationContract]
        void ServiceMethod(string clientThreadID);
    }
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.Single)]
    public class ReentrantSingle : IReentrantSingle
    {
        public void ServiceMethod(string clientThreadID)
        {
            IReentrantSingleCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantSingleCallBack>();
            Debug.WriteLine("服務器端 " + this.GetHashCode() + ",客戶端線程" + clientThreadID + ": 準備開始客戶端回調......" + DateTime.Now.ToString("hh:mm:ss ms"));
            callback.ClientMethod();
            Debug.WriteLine("服務器端 " + this.GetHashCode() + ",客戶端線程" + clientThreadID + ":客戶端回調結束。" + DateTime.Now.ToString("hh:mm:ss ms"));
        }
    }
客戶端代碼:
    class ReentrantSingle : IReentrantSingleCallback
    {
        private static ReentrantSingle instance = new ReentrantSingle();
        private static InstanceContext context = new InstanceContext(instance);
        private static SRReentrantSingle.ReentrantSingleClient client = new Client.SRReentrantSingle.ReentrantSingleClient(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() + "發起調用......");
            client.ServiceMethod(Thread.CurrentThread.ManagedThreadId.ToString()+"-"+Guid.NewGuid().ToString());
            Console.WriteLine("客戶端線程" + Thread.CurrentThread.GetHashCode() + "調用結束....../n");
        }
    }

運行效果



《圖6-1》
從圖中我們看出兩個客戶端的六個線程的確調用服務端的同一個實例,用藍色和綠色標出來的執行結果分別是不同客戶端的線程回調情況。我們可以看出兩個客戶端之間是並行運行的,而同一個客戶端的三個線程則是排隊調用的--因爲用的是“請求-響應”的調用模式。

到目前爲至我們研究完了PerCall和Reentrant兩種併發模式,這兩種併發模式都是單線程調用,每個調用都會獨佔其服務,因此並不會對服務實例產生併發調用的衝突。

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



《圖7》
對於Multiple併發模型,應當是多線程的併發訪問模式,但對於PerCall實例模型中,每個線程與一個獨立的服務實例進行交互,所以一般不會產生併發衝突。但如果服務實例中使用了靜態變量或全局數據緩存的時候,在多線程操作這些共享資源時需要手動編寫同步代碼,以確保這些共享資源不會發生併發衝突。

服務器端代碼:
    [ServiceContract]
    public interface IMultiplePerCall
    {
        [OperationContract(IsOneWay = true)]
        void TestService(string clientThreadID);
    }
    [ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Multiple, InstanceContextMode=InstanceContextMode.PerCall)]
    public class MultiplePerCall : IMultiplePerCall
    {
        private int _InstanceState = 0;
        private static int _StaticState = 0;
        public void TestService(string clientThreadID)
        {
            Random rand = new Random();
            while (true)
            {
                _InstanceState++;
                _StaticState = _InstanceState;
                Thread.Sleep(rand.Next(500));
                Debug.WriteLine("實例" + this.GetHashCode().ToString() + ": " + clientThreadID.ToString() + " ;狀態的值是:" + _StaticState.ToString() + " - " + _InstanceState.ToString());
            }
        }
    }
    在這段代碼中,我們對服務端的調用依然使用OneWay方式。在類中我們設置了兩個成員變量,一個是實例變量,一個是靜態變量。由於我們使用PerCall實例模型,每個線程對應一個服務實例,對於實例變量應當不會產生併發衝突的問題,但對於靜態變量,由於使用的是Multiple併發模型,有可能會發生多個實例對象同時修改同一個靜態變量的問題。
    爲了演示這種併發效果我們使用了一個死循環:
    while (true)
    {
        _InstanceState++;
        _StaticState = _InstanceState;
        Thread.Sleep(rand.Next(500));
        Debug.WriteLine("實例" + this.GetHashCode().ToString() + ": " + clientThreadID.ToString() + " ;狀態的值是:" + _StaticState.ToString() + " - " + _InstanceState.ToString());
    }
    在循環中,我們使用實例變量的值來修改靜態變量,並在修改完後線程休眠500毫秒,以讓出處理器讓其它線程執行處理,然後再顯示我們剛纔賦值的結果。
    如果沒有產生併發衝突,應當顯示實例變量和靜態變量相等。如果產生了併發衝突,那在當前實例執行完_StaticState = _InstanceState後,_StaticState變量的值又會被其它服務實例修改,最後會導實例變量和靜態變量的值不一致的結果。
   
客戶端代碼:
    class MultiplePerCall
    {
        private static SRMultiplePerCall.MultiplePerCallClient client = new Client.SRMultiplePerCall.MultiplePerCallClient();
        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()
        {
            client.TestService(Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }
    一個代理實例使用三個線程調用服務。
運行結果:



《圖7-1》
從運行結果上來看,
服務端產生了三個實例58366981,56140151和37916227
每個服務的實例變量的值(橫線右邊的狀態值)永遠是不重複遞增,即實例變量不會產生併發衝突。
實例變量(橫線右邊的狀態值)和靜態變量(橫線左邊的狀態值)不一致,說明在_StaticState = _InstanceState後,靜態變量又被其它服務實例修改過,即產生了併發衝突。

要想防止多個服務實例對靜態變量修改操作產生併發衝突,需要我們手寫代碼對它進行同步保護
服務端代碼做如下修改
    public class MultiplePerCall : IMultiplePerCall
    {
         ......
            while (true)
            {
                lock (typeof(MultiplePerCall))
                {
                    ....

                }
            }
        }
    }
使用lock語句段,把變量修改與顯示的代碼鎖定在一起,防止其實服務實例的併發修改衝突。


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



《圖8》
“Multiple併發+PerSession實例模型”會對每個客戶端的多線程請求指派一個服務實例進行處理,多線程的請求同一個服務時,同樣並不會自動鎖定該服務,對於服務中的數據進行修改的時候也需要我們手動編寫同步代碼進行保護。對於不同的客戶端雖然對應不同的服務實例,但這些服務實例有可能共享靜態數據或全局緩存數據,如果需要對這些數據進行操作的時候也需要手動編寫代碼進行同步保護。

服務端代碼:
   [ServiceContract(SessionMode= SessionMode.Required)]
    public interface IMultiplePerSession
    {
        [OperationContract(IsOneWay=true)]
        void TestService(string clientThreadID);
    }
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.PerSession)]
    public class MultiplePerSession : IMultiplePerSession
    {
        private int _InstanceState = 0;
        private int _StateValue = 0;
        public void TestService(string clientThreadID)
        {
            Random rand = new Random();
            while (true)
            {
                _InstanceState++;
                Thread.Sleep(rand.Next(500));
                _StateValue = _InstanceState;
                Debug.WriteLine("實例" + this.GetHashCode().ToString() + ": " + clientThreadID.ToString() + " 狀態值:" + _InstanceState + " - " + _StateValue);
            }
        }
    }
    在這時我們仍使用OneWay的調用模式,代碼設計的思想與上面中設計的思想相似,只是休眠的位置不是在賦完值後休眠,而是在賦值之前休眠。如果產生併發的話,在休眠的話,就會把我剛賦好的值覆蓋掉,出現變量的值不連續或數據重複的情況。
   
客戶端代碼:
    class MultiplePerSession
    {
        private static SRMultiplePerSession.MultiplePerSessionClient client = new Client.SRMultiplePerSession.MultiplePerSessionClient();
        public static void Main8(string[] args)
        {
            for (int i = 0; i < 3; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();
            }
            Console.ReadLine();
        }
        public static void DoWork()
        {
            client.TestService(Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }
    這裏我們同時啓動兩個客戶端,每個客戶端使用三個線程向服務端發起調用。
運行結果:



《圖8-1》
從圖中我們看出,服務端的確有兩個服務在運行(42931033和20974680),這兩個服務實例分別出現了一次數據重複。並且是同一服務實例內的數據重複,即說明了同一客戶端的多線程調用發生了併發衝突。
防止併發衝突的同步鎖定代碼:
   lock (this)
    {

       ...
    }

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



《圖9》
這種併發模型與實例模型的匹配算是多線程併發操作的一個極佳搭配。在Single實例模型中,所有客戶端的所有線程共享一個服務實例,這樣可以很好地解決多個客戶端的不同線程間的狀態共享的問題。可是這種實例模型的吞吐量會小於PerCall實例模型的吞吐量,因此我們可以使用Multiple併發模型來增加其吞吐量,可以允許多個客戶端線程對服務實例同時調用。但這樣也最容易對一些共享數據和共享資源引起併發衝突,所以需要我們手動編寫代碼對共享數據和共享資源進行併發保護。
服務端代碼:
    [ServiceContract]
    public interface IMultipleSingle
    {
       [OperationContract( IsOneWay=true)]
        void TestService(string clientThreadID);
    }

    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)]
    public class MultipleSingle : IMultipleSingle
    {
        private int _InstanceState = 0;
        public void TestService(string clientThreadID)
        {
            Random rand = new Random();
            while (true)
            {
                _InstanceState++;
                Thread.Sleep(rand.Next(10));
                Trace.WriteLine(string.Format("實例{0},線程{1}:狀態量的值是 {2};", this.GetHashCode(), clientThreadID, _InstanceState));
               
            }
        }
    }
    在這裏我只使用了實例變量,沒有使用靜態變量,多線程的客戶端會對該服務實例產生併發衝突。在爲實例變量賦完值後,我休眠一段時間,再顯示賦值的結果,結果會在休眠的這段時間內其它線程來修改變量的值導致重複數據的產生。
   
客戶端代碼:
    class MultipleSingle
    {
        private static SRMultipleSingle.MultipleSingleClient client = new Client.SRMultipleSingle.MultipleSingleClient();
        public static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();
            }
            Console.ReadLine();
        }
        public static void DoWork()
        {
            client.TestService(Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }
運行結果:



《圖9-1》
從上面圖中看出,服務端只有一個服務實例,三個線程同時運行,產生併發衝突。
   
總結上面的內容,增加系統吞吐量可以提升服務的效率。但影響吞吐量的因素比較多,如:實例模型、併發模型和調用模型等。
限流模型也會對吞吐量造成影響。限流是爲了控制客戶端或服務端相關資源的數量,以使服務處於最佳的運行狀態。

三、限流

可以配置服務行爲的serviceThrottling屬性來對WCF服進行限流。
MaxConcurrentCalls:限制客戶端發起的併發請求數量 – 缺省爲16
MaxConcurrentInstances:限制服務端服務實例的數量。
MaxConcurrentSessions:限制活動會話數量-缺省值10



《圖10》
代碼:



《圖11》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章