承接上篇,本文將呈現一個簡單但功能完整的 Actor 模型核心實作。採用事件驅動分派,直接利用 .NET 內建線程池執行任務。關鍵在於兩大原則:訊息存在即儘早執行、單一 Actor 訊息循序執行。由此衍生兩個將 Actor 推入線程池的時機:收到訊息且 Actor 處於等待狀態、執行完訊息且信箱仍有訊息。並發控制稍有不慎即會導致 Actor 卡死、重複執行或空跑,文中將逐步拆解無鎖同步機制。
對外暴露三個核心功能:投遞訊息(Post)、處理訊息(Receive)、退出迴圈(Exit)。內部則透過 IActor 介面統一調度:
internal interface IActor
{
void Execute();
bool Existed { get; }
int MessageCount { get; }
ActorContext Context { get; }
}
ActorContext 封裝同步所需的原始欄位(如 m_status),避免對屬性/方法進行 CAS 操作的限制,同時解決 F# 存取基類 internal 成員的 Bug。
Actor 三態:WAITING (0)、EXECUTING (1)、EXITED (2)。Dispatcher.ReadyToExecute 以 Interlocked.CompareExchange 原子性地將狀態從 WAITING 切為 EXECUTING,成功才將 Actor 推入線程池:
public void ReadyToExecute(IActor actor)
{
if (actor.Existed) return;
int status = Interlocked.CompareExchange(
ref actor.Context.m_status,
ActorContext.EXECUTING,
ActorContext.WAITING);
if (status == ActorContext.WAITING)
ThreadPool.QueueUserWorkItem(this.Execute, actor);
}
Post 將訊息入佇列後呼叫 ReadyToExecute;Execute 取出訊息呼叫使用者的 Receive,執行完畢後視狀態決定是否重新排程:
void IActor.Execute()
{
T message;
lock (this.m_messageQueue) { message = this.m_messageQueue.Dequeue(); }
this.Receive(message);
}
private void Execute(object o)
{
IActor actor = (IActor)o;
actor.Execute();
if (actor.Existed)
{
Thread.VolatileWrite(ref actor.Context.m_status, ActorContext.EXITED);
}
else
{
Thread.VolatileWrite(ref actor.Context.m_status, ActorContext.WAITING);
if (actor.MessageCount > 0) this.ReadyToExecute(actor);
}
}
關鍵在於 VolatileWrite 後再檢查 MessageCount,雖中間可能有新訊息到達觸發並行 ReadyToExecute,但 CAS 機制保證只會有一個成功,邏輯完備。
public class Counter : Actor
{
private int m_value;
public Counter() : this(0) { }
public Counter(int initial) { this.m_value = initial; }
protected override void Receive(int message)
{
this.m_value += message;
if (message == -1)
{
Console.WriteLine(this.m_value);
this.Exit();
}
}
}
static void Main()
{
Counter counter = new Counter();
for (int i = 0; i < 10000; i++) counter.Post(i);
counter.Post(-1);
Console.ReadLine();
}
單一 Actor 內無並發,業務邏輯純淨無鎖。完整實作不足 150 行,下一篇將結合 F# 展現更強表達力並進行效能剖析。