前陣子有個朋友問了我一個有趣的問題,他們想要做一個獨立於 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 裡的執行順序是會保持正常的。
詳細的程式碼可在這裡取得。