續創造一個訊息迴圈 -- Proxy Pattern

  • 59
  • 0
  • 2021-11-16

前一篇我們簡單做出了一個訊息迴圈的函式庫,但應用上因為呼叫的方式必須要傳入一些額外的參數,難免讓人覺得美中不足。這次我們改用 Proxy Pattern 來實作看看。

想法其實也滿簡單,利用 RealProxy Class 來為外部呼叫創造一個透明代理,透過覆寫 RealProxy.Invoke method 來轉到自訂的訊息迴圈裡運行。為了搭配 RealProxy class,程式碼要做一些小小的更動。

狀態管理物件

這邊需要一個管理被加入到特定執行緒方法執行的管理物件,為此設計一個類別:

 private class WorkData
 {
     public object Target { get; }

     public IMethodCallMessage Call { get; }

     public bool IsCompleted { get; set; }
    
     public object Result { get; set; }

     public WorkData(object target, IMethodCallMessage call)
     {
         IsCompleted = false;
         Target = target;
         Call = call;
     }
 }

(1) Target 代表執行個體方法所需之執行個體。

(2) Call 則是由 RealProxy 所攔截的 Call Message。

(3) IsCompleted 指示方法是否已經執行完畢

(4) Result 則是方法執行後的結果

SingleThreadWorker
 private sealed class SingleThreadWorker
 {
     private Thread _thread;
     private AutoResetEvent _resetEvent;
     public bool IsRunning { get; private set; }

     private ConcurrentQueue<WorkData> _delegates;

     public SingleThreadWorker()
     {
         IsRunning = true;
         _delegates = new ConcurrentQueue<WorkData>();
         _resetEvent = new AutoResetEvent(false);
         _thread = new Thread(RunMessageLoop);
         _thread.IsBackground = true;
         _thread.Start();
     }


     public object Call(object target, IMethodCallMessage call)
     {                
         var spin = new SpinWait();
         var data = new WorkData(target, call);
         _delegates.Enqueue(data);
         _resetEvent.Set();
         while (!data.IsCompleted)
         {
             spin.SpinOnce();
         }
         return data.Result;
     }



     private void Stop()
     {
         IsRunning = false;
         _resetEvent?.Set();
     }

     private void RunMessageLoop()
     {
         while (IsRunning)
         {
             while (_delegates.TryDequeue(out WorkData data))
             { 
                 data.Result = (data.Call.MethodBase as MethodInfo).Invoke(data.Target, data.Call.InArgs);
                 data.IsCompleted = true;
             }

             if (_delegates.Count == 0 && IsRunning)
             {
                 _resetEvent.WaitOne();
             }
         }
     }

     ~SingleThreadWorker()
     {
         Stop();
     }

 }

這邊主要做了幾個改變。

(1) ConcurrentQueue 的元素型別改為 WorkData。

(2) RunMessageLoop 直接從 WorkData 的 Call 屬性取得需要的方法與參數,並且將結果設定給 WorkData 的 Result 屬性。

(3) Call 方法的部分,利用 SpinWait 等待 WorkData 指示方法是否已經完成。

Proxy

各位或許會注意到以上兩個類別都被設定為 private,因為設計上我認為這兩個類別不必為外界所知,所以設計為 Proxy 內的巢狀類別。

 public class MessageProxy<T> : RealProxy where T : class
 {
     private readonly T _target;
     private SingleThreadWorker _worker;

     public MessageProxy(T target) : base(typeof(T))
     {
         _target = target;
         _worker = new SingleThreadWorker();
     }

     public override IMessage Invoke(IMessage message)
     {
         var call = message as IMethodCallMessage;
         var result = _worker.Call(_target, call);
         return new ReturnMessage(result, null, 0, call.LogicalCallContext, call);
     }


     private sealed class SingleThreadWorker
        {
            private Thread _thread;
            private AutoResetEvent _resetEvent;
            public bool IsRunning { get; private set; }

            private ConcurrentQueue<WorkData> _delegates;

            public SingleThreadWorker()
            {
                IsRunning = true;
                _delegates = new ConcurrentQueue<WorkData>();
                _resetEvent = new AutoResetEvent(false);
                _thread = new Thread(RunMessageLoop);
                _thread.IsBackground = true;
                _thread.Start();
            }


            public object Call(object target, IMethodCallMessage call)
            {                
                var spin = new SpinWait();
                var data = new WorkData(target, call);
                _delegates.Enqueue(data);
                _resetEvent.Set();
                while (!data.IsCompleted)
                {
                    spin.SpinOnce();
                }
                return data.Result;
            }



            private void Stop()
            {
                IsRunning = false;
                _resetEvent?.Set();
            }

            private void RunMessageLoop()
            {
                while (IsRunning)
                {
                    while (_delegates.TryDequeue(out WorkData data))
                    { 
                        data.Result = (data.Call.MethodBase as MethodInfo).Invoke(data.Target, data.Call.InArgs);
                        data.IsCompleted = true;
                    }

                    if (_delegates.Count == 0 && IsRunning)
                    {
                        _resetEvent.WaitOne();
                    }
                }
            }

            ~SingleThreadWorker()
            {
                Stop();
            }

        }


     private class WorkData
        {
            public object Target { get; }

            public IMethodCallMessage Call { get; }

            public bool IsCompleted { get; set; }
           
            public object Result { get; set; }

            public WorkData(object target, IMethodCallMessage call)
            {
                IsCompleted = false;
                Target = target;
                Call = call;
            }
        }
 }

Proxy 的內容很簡單,收到 Call Message 就轉傳給 SingleThreadWroker 執行。

應用範例一

先拿個 Console Application 來試試這玩意怎麼用。

 class Program
    {
        static void Main(string[] args)
        {
            var proxy001 = new MessageProxy<MyClass>(new MyClass()).GetTransparentProxy() as MyClass;
            Call(proxy001, nameof(proxy001));
            var proxy002 = new MessageProxy<MyClass>(new MyClass()).GetTransparentProxy() as MyClass;
            Call(proxy002, nameof(proxy002));
            Console.ReadLine();
        }


        private static void Call(MyClass proxy, string proxyName)
        {
            proxy.Method001(proxyName);
            for (int i = 0; i < 3; i++)
            {
                CallMethod002(proxy, proxyName);
            }
        }
        private static void CallMethod002(MyClass proxied, string proxyName)
        {
            var result = proxied.Method002(proxyName);
            Console.WriteLine($"Count is {result}");
        }
    }

 class MyClass  : MarshalByRefObject
    {
        private int _count = 0;
        public void Method001(string caller)
        {
            Console.WriteLine($"Method 001 running in Thread Id {Thread.CurrentThread.ManagedThreadId} by {caller}");
        }

        public int Method002(string caller)
        {
            _count++;
            Console.WriteLine($"Method 002 running in Thread Id {Thread.CurrentThread.ManagedThreadId}  by {caller}");
            return _count;
        }
    }
應用範例二

這設計其實有一個弔詭的地方,在 SingleThreadWroker.Call method 裡因為要同步化用上了 SpinWait,這會導致呼叫端的執行緒被占用,因此當方法的執行時間很長的時候,可能需要多一層非同步包裝,如以下 Windows Forms Application 程式碼所示。

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        MyClass proxy001 = new MessageProxy<MyClass>(new MyClass()).GetTransparentProxy() as MyClass;

        async private void button1_Click(object sender, EventArgs e)
        {
            await Task.Run(() => proxy001.Method001(nameof(proxy001)));
        }

        async private void button2_Click(object sender, EventArgs e)
        {
            int count = await Task.Run(() => proxy001.Method002(nameof(proxy001)));
            MessageBox.Show($"count is {count}");
        }
    }

    class MyClass : MarshalByRefObject
    {
        private int _count = 0;
        public void Method001(string caller)
        {

            Debug.WriteLine($"Method 001 running in Thread Id {Thread.CurrentThread.ManagedThreadId} by {caller}");
            Thread.Sleep(10000);
            Debug.WriteLine($"Method 001 running in Thread Id {Thread.CurrentThread.ManagedThreadId} end");
        }

        public int Method002(string caller)
        {
            _count++;
            Debug.WriteLine($"Method 002 running in Thread Id {Thread.CurrentThread.ManagedThreadId}  by {caller}");
            return _count;
        }
    }
結語

利用 RealProxy 有個好處是對於呼叫端來說顯得呼叫的方式比較自然,不像前一篇的做法需要產一堆引數來傳遞;但這也絕非全然沒有缺點,第一個就是對長時執行的方法而言,呼叫端得自己考慮非同步呼叫;第二個是這僅適用於執行個體方法,如果對象是靜態方法,就得再多包一層;第三是 RealProxy 對於型別的限制。各有優缺,依照實際情境酌量取用。或是你也可以試試另外一種方式 – 採用靜態代理。

整個範例可在此取得

學設計模式所為何事?當然是必要時拿出來用用啊。