隨著多核普及,客戶端也必須面對並發。正確的並發代碼需遵循額外規則:同步存取共享資料、協調執行緒推進。同步引入非確定性,難測試、難預測。本文梳理 11 大常見隱患與應對模式。
多執行緒同時讀寫共享記憶體,且至少一個寫入。經典計數器失效:
static int s_curr = 0;
int GetNext() { return s_curr++; } // 非原子:讀-增-寫三步驟
解法:Interlocked.Increment 或 lock。
最簡單的競爭:完全沒加鎖。常見於物件從私有發布為共享時 (發布/釋出)。解法:識別共享點,加入正確同步。
鎖範圍太小,無法保護不變量。轉帳範例:
// 錯誤:資金中間狀態丟失
Withdraw(a, delta); Deposit(b, delta);
// 正確:預先鎖定雙帳戶
lock(a.Lock) lock(b.Lock) { Withdraw(a, delta); Deposit(b, delta); }
但引發死鎖風險 (見第 6 項)。
非對齊或超過原生字長 (32/64 位) 讀寫非原子。64 位變數在 32 位系統可能讀到高低半字混合值。解法:確保對齊、用 Interlocked.Read 或鎖保護。
編譯器/CPU 重排序記憶體操作導致邏輯錯誤。雙重檢查鎖定失效經典案例。解法:Thread.MemoryBarrier、volatile、或干脆用鎖。未徹底理解記憶體模型前,避免無鎖編程。
CLR Monitor 支援遞歸。若鎖區域呼叫虛擬方法/委派,可能觸發重入破壞不變量。最佳實踐:鎖內不呼叫動態代碼。
循環等待鏈。解法:一致鎖獲取順序 (如按帳戶 ID 排序)。
if (a.Id < b.Id) { lock(a) lock(b) ... } else { lock(b) lock(a) ... }
事件信號被忽略 (自動重置事件重複 Set)、或條件變數用 Pulse 應用 PulseAll 場景。檢查等待條件重置邏輯。
熱鎖導致排隊、線程飢餓。ASP.NET 常見於短時工作項大量排隊。緩解:減少鎖持有時間、分解熱鎖、無鎖資料結構。
大量線程同時被喚醒搶佔 CPU,多數發現條件不滿足再睡。手動重置事件觸發單項工作時易發生。解法:信號量、工作佇列模式。
readonly、F# 預設不可變Freezable 實現動態不可變public class ImmutableStack{ readonly T value; readonly ImmutableStack next; readonly bool empty; public ImmutableStack Push(T v) => new ImmutableStack(v, this); public ImmutableStack Pop(out T v) { v = value; return next; } }
並發核心難點在於**共享可變狀態**。採用不可變資料、純函數、執行緒隔離三大模式,從根源消除同步需求。現代語言 (F#、Rust、Go) 皆以此為核心設計哲學。