前一篇我們簡單做出了一個訊息迴圈的函式庫,但應用上因為呼叫的方式必須要傳入一些額外的參數,難免讓人覺得美中不足。這次我們改用 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 對於型別的限制。各有優缺,依照實際情境酌量取用。或是你也可以試試另外一種方式 – 採用靜態代理。
整個範例可在此取得。