跟ASP.NET WebForm或MVC Controller一樣,WCF在接收Client的呼叫時,Server端必須建立一個Service型別的Instacne(執行個體)執行作業。在WebForm或MVC Controller裡,多採行「為每次Request建立Instance,處理完畢就抛棄」的策略,這與HTTP協定的無狀態(Stateless)特性有關。WCF Service因支援雙向呼叫等應用模型,故WCF Service Instance的生命週期管理比一般ASP.NET網頁複雜。

透過[ServiceBehavior(InstanceContextMode=…)],我們可以指定Service Instance採行以下三種策略:

  • PerCall:每次接受呼叫就新建一個Instance,跟WebForm或MVC Controller概念一致。
  • PerSession:為每個Session建立一個Instance,用該Instance處理該Session的所有呼叫。
    註:WCF Session的定義是從Clinet端new TheWcfProxyClient()開始,一直到它.Close()結束或Dispose()被回收為止。另外,要啟用Session需選用Ws*Binding、NetTcpBinding、NetNamedPipeBinding等管道,像BasicHttpBinding就不支援。(參考:WCF預設Binding種類
  • Single:從頭到尾只會建一個Instance,服務所有呼叫要求。

PerCall走的是無狀態(Stateless)哲學,所有呼叫動作彼此獨立無關聯,故可輕易透過擴充伺服器數量提高系統負荷量。當Instance建立成本較高,或在多次呼叫動作間需保留狀態,就必須考慮改用PerSession或Single方式。而雙向式WCF作業,Server端需要一個持續存在的Instance主動向Client發動呼叫,故需使用PerSession或Single。(延伸閱讀:WCF Sessions - Brief Introduction - CodeProject

除了InstanceContextMode,ServiceBehavior還有另一個參數ConcurrencyMode,ConcurrencyMode有三個選項:

  • Single:WCF在執行作業時會鎖定Service Context(包含Service Instance),限定單一時間只能由一條執行緒存取,如此可避免非同步作業衍生的資源存取衝突。但需留意如此限定對該Instance的所有作業只能循序執行,上演以前提過的ASP.NET大排長龍效應
  • Multiple:允許多執行緒同時存取,可達最高效能,但如共用Instance或其他資源,開發者必須使用lock等機制自行確保Thread-Safe。
  • Reentrant:類似Single,不允許多執行緒同時存取Instance,但該Instance向外呼叫其他WCF服務又回頭對該Instance發動的呼叫則不在此限,以避免Deadlock。
    延伸閱讀:Chapter 8. Concurrency Management

了解原理後,免不了要實驗驗證。我設計如下的Service1.svc.cs,Instance建立時以GUID隨機命名,用來區別不同的Instance。ShowInstanceAndThread()作業會延遲一秒再傳回Instance名稱及當下執行的Thread ID,延遲一秒目的在突顯呼叫為循序執行還是同時並行。在Service1類別我們加上ServiceBehavior Attribute,切換不同InstanceContextMode及ConcurrencyMode,以觀察不同模式下的執行結果。

using System;
using System.ServiceModel;
using System.Threading;
 
namespace WcfTest
{
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        string ShowInstanceAndThread();
    }
 
    [ServiceBehavior(
        InstanceContextMode = InstanceContextMode.PerCall,
        ConcurrencyMode = ConcurrencyMode.Single
    )]
    public class Service1 : IService1
    {
        //Instance建立時隨機決定instanceName
        string instanceName = Guid.NewGuid().ToString().Substring(0, 4);
        public string ShowInstanceAndThread()
        {
            Thread.Sleep(1000);
            return string.Format("{0}/Thread:{1}/Time:{2:HH:mm:ss.fff}",
                instanceName, Thread.CurrentThread.ManagedThreadId, DateTime.Now);
        }
    }
}

由於預設的basicHttpBinding不支援Session,web.config要修改一下:(註:我在本機測試,故忽略驗證問題,否則還需加上<security mode="None" />設定,相關細節請參閱前文)

    <protocolMapping>
        <add binding="wsHttpBinding" scheme="http"/>
    </protocolMapping>  

WCF Client加入參照後,程式中同時建立兩個Service1Client()(在PerSession模式會在Server端對應兩個專屬Service Instance),先用client.Open()跟Server建立連線,之後用Parallel.For同時送出三次ShowInstanceAndThread()呼叫,由於非同步執行無法預期執行順序,這裡用了點小技巧,讓第一次呼叫Delay 50ms,第二次Delay 100ms,第三次Delay 150ms,確保三次呼叫先後送出:

        static Task RunTest(string name)
        {
            return Task.Factory.StartNew(() =>
            {
                var client = new WcfTest.Service1Client();
                //呼叫client.Open(),確保WCF服務就緒
                client.Open();
                Parallel.For(1, 4, (j) =>
                {
                    //加上小小的延遲,確保先發j=1,再依序發2,3
                    Thread.Sleep(j * 50);
                    Console.WriteLine("N={0},J={1}->{2}",
                        name, j, client.ShowInstanceAndThread());
                });
            });
        }
 
        static void Main(string[] args)
        {
            var jobs = new List<Task>();
            for (var i = 1; i <= 2; i++)
            {
                jobs.Add(RunTest("Client" + i));
            }
            Task.WaitAll(jobs.ToArray());
            Console.Read();
            
        }

準備就緒,來看看不同InstanceContextMode及ConcurrencyMode的測試結果。

1. InstanceContextMode.PerCall + ConcurrencyMode.Multiple

N=Client2,J=2->8f3e/Thread:17/Time:15:43:41.062
N=Client1,J=3->abf9/Thread:22/Time:15:43:41.063
N=Client2,J=1->d99a/Thread:11/Time:15:43:41.059
N=Client1,J=1->a66b/Thread:12/Time:15:43:41.059
N=Client2,J=3->21c7/Thread:14/Time:15:43:41.063
N=Client1,J=2->8131/Thread:15/Time:15:43:41.060

PerCall時每次呼叫用的Instance都不同,同時執行。

2. InstanceContextMode.PerCall + ConcurrencyMode.Single

N=Client2,J=1->99c9/Thread:11/Time:15:45:43.497
N=Client1,J=1->525a/Thread:9/Time:15:45:43.497
N=Client2,J=2->ac78/Thread:11/Time:15:45:44.506
N=Client1,J=2->0a2b/Thread:9/Time:15:45:44.505
N=Client1,J=3->2c51/Thread:9/Time:15:45:45.513
N=Client2,J=3->96be/Thread:11/Time:15:45:45.514

指定ConcurrencyMode.Single,出現Client2固定用Thread 11,Client1固定用Thread 9的結果,雖然Instance每次不同,但WCF鎖定的對象為整個Service Contexty,Client1/Client2的三次呼叫循序跑完,耗時三秒。

3. InstanceContextMode.PerSession + ConcurrencyMode.Multiple

N=Client2,J=1->9dae/Thread:11/Time:15:47:20.290
N=Client1,J=2->3a09/Thread:14/Time:15:47:20.293
N=Client1,J=3->3a09/Thread:13/Time:15:47:20.293
N=Client2,J=3->9dae/Thread:17/Time:15:47:20.293
N=Client1,J=1->3a09/Thread:9/Time:15:47:20.290
N=Client2,J=2->9dae/Thread:15/Time:15:47:20.291

看出PerSession的效果了,Client1固定用Instance 3a08,Client2固定用9dae,但六次呼叫同時跑完。

4. InstanceContextMode.PerSession + ConcurrencyMode.Single

N=Client2,J=1->df72/Thread:10/Time:15:48:22.361
N=Client1,J=1->26ab/Thread:9/Time:15:48:22.361
N=Client2,J=2->df72/Thread:10/Time:15:48:23.372
N=Client1,J=2->26ab/Thread:9/Time:15:48:23.370
N=Client2,J=3->df72/Thread:10/Time:15:48:24.380
N=Client1,J=3->26ab/Thread:9/Time:15:48:24.381

Client1固定用Instance 26ab,Client2固定用df72,各自的三次呼叫循序跑完。

5. InstanceContextMode.Single + ConcurrencyMode.Multiple

N=Client1,J=2->3df5/Thread:14/Time:15:49:22.760
N=Client2,J=1->3df5/Thread:9/Time:15:49:22.759
N=Client2,J=3->3df5/Thread:21/Time:15:49:22.764
N=Client1,J=1->3df5/Thread:8/Time:15:49:22.759
N=Client2,J=2->3df5/Thread:10/Time:15:49:22.762
N=Client1,J=3->3df5/Thread:13/Time:15:49:22.762

Client1與Client2共用Instance 3df5,六次呼叫同時跑完。

6. InstanceContextMode.Single + ConcurrencyMode.Single

N=Client1,J=1->8044/Thread:14/Time:15:50:16.621
N=Client2,J=1->8044/Thread:13/Time:15:50:17.626
N=Client1,J=2->8044/Thread:14/Time:15:50:18.628
N=Client2,J=2->8044/Thread:13/Time:15:50:19.631
N=Client1,J=3->8044/Thread:14/Time:15:50:20.637
N=Client2,J=3->8044/Thread:14/Time:15:50:21.642

Client1與Client2共用Instance 8044,六次呼叫排成一列,花六秒慢慢消化。

由以上結果,我們觀察到InstanceContextMode及ConcurrencyMode對Instance管理及執行順序的影響,可做為日後設計WCF服務的參考。


Comments

Be the first to post a comment

Post a comment