創造一個訊息迴圈

  • 438
  • 0
  • 2021-11-11

前陣子有個朋友問了我一個有趣的問題,他們想要做一個獨立於 UI 執行緒外的永不停止執行緒,這個執行緒等待接收外部的訊號並呼叫對應的 API 執行;用個名詞來說就是一個獨立的訊息迴圈,我大概想了幾個做法,今天就來聊聊其中一個想法的實踐。

基本想法是這樣:創造一個執行緒,裡面當然是一個可以長時運轉的迴圈,再加上等待訊號和一個訊息佇列。先來看程式碼:

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

        private ConcurrentQueue<(object target, Delegate method, object[] args, Action<object> callback)> _delegates;

        public SingleThreadWorker()
        {
            IsRunning = true;
            _delegates = new ConcurrentQueue<(object target, Delegate method, object[] args, Action<object> callback)>();
            _resetEvent = new AutoResetEvent(false);
            _thread = new Thread(RunMessageLoop);
            _thread.IsBackground = true;
            _thread.Start();
        }


        public void Call(object target, Delegate method, object[] args, Action<object> callback)
        {
            _delegates.Enqueue((target, method, args, callback));
            _resetEvent.Set();
        }

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

        private void RunMessageLoop()
        {
            while (IsRunning)
            {
                while (_delegates.TryDequeue(out (object target, Delegate method, object[] args, Action<object> callback) item))
                {
                    object result = item.method.Method.Invoke(item.target, item.args);
                    item.callback?.Invoke(result);
                }

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

        ~SingleThreadWorker()
        {
            Stop();
        }

    }

程式碼的運作邏輯是呼叫建構式的時候產生一個迴圈,這個迴圈會取用 _delegates 裡的資料並加以執行若 _delegates 目前全部執行完畢則會進入到等待訊號 _resetEvent.WaitOne()    暫時停止。

外部程式呼叫 Call method,傳入的參數分別為 (1) 執行個體,若為靜態方法則傳入 null (2) 委派,指向欲執行的方法 (3) 方法的參數 (4) 執行後的回呼委派,其中的參數是當執行方法有回傳值的時候可以藉此傳出去;如果熟悉使用反射呼叫方法的人,對這排參數的定義應該不陌生。

傳入的資料會被放進名稱為 _delegates 的 ConcurrentQueue,接著呼叫 Set 讓等待訊號放行。

非常簡單的邏輯,對吧?

我們可以寫一個 Console 函式來測試看看是否不同的呼叫依然會留在同一個執行緒:

 class Program
 {
     static void Main(string[] args)
     {
         var worker = new SingleThreadWorker();
         worker.Call(null,new Action<string> (Method001), new object[] { nameof(worker) }, null);
         worker.Call(null, new Func<string,int>(Method002), new object[] { nameof(worker) }, (x) =>  Console.WriteLine($" count is  {x}"));
         worker.Call(null, new Func<string,int>(Method002), new object[] { nameof(worker) }, (x) => Console.WriteLine($" count is  {x}"));

        

         var anotherWroker = new SingleThreadWorker();
         anotherWroker.Call(null, new Action<string>(Method001), new object[] { nameof(anotherWroker) }, null);
         anotherWroker.Call(null, new Func<string, int>(Method002), new object[] { nameof(anotherWroker) }, (x) => Console.WriteLine($" count is  {x}"));
         anotherWroker.Call(null, new Func<string, int>(Method002), new object[] { nameof(anotherWroker) }, (x) => Console.WriteLine($" count is  {x}"));
         Console.ReadLine();
     }

     static int _count = 0;
     static void Method001(string caller)
     {
         Console.WriteLine($"Method 001 running in Thread Id {Thread.CurrentThread.ManagedThreadId} by {caller}");
     }

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

 }
執行結果

因為執行緒的關係,worker 和 anotherWorker 可能順序會有所不同,但在同一個 SingleThreadWorker 裡的執行順序是會保持正常的。

詳細的程式碼可在這裡取得