多執行緒共享資源時必須協調存取。.NET 提供豐富的同步原語,本文系統介紹十大類同步機制,涵蓋原子操作、鎖、監視器、事件、互斥體、讀寫鎖、信號量、障礙、自旋鎖與自旋等待,並以「共享印表機」、「做飯」、「購票」等經典案例說明適用場景。
| 類別 | .NET 類別/關鍵字 | 底層模型 |
|---|---|---|
| 臨界區 | lock, Monitor, SpinLock | 互斥鎖 |
| 互斥量 | Mutex | 核心互斥體 |
| 信號量 | Semaphore, SemaphoreSlim | 計數信號量 |
| 事件 | AutoResetEvent, ManualResetEvent, Barrier | 事件通知 |
| 原子操作 | Interlocked | CPU 原子指令 |
| 讀寫鎖 | ReaderWriterLockSlim | 共享/獨佔鎖 |
適用於計數器、旗標等簡單整數變數。`Increment`、`Decrement`、`Exchange`、`CompareExchange` 皆為原子性。
// 共享印表機旗標
static int UsingPrinter = 0;
if (0 == Interlocked.Exchange(ref UsingPrinter, 1)) {
// 取得印表機使用權
// ...
Interlocked.Exchange(ref UsingPrinter, 0); // 釋放
}
`lock(obj)` 編譯為 `Monitor.Enter/Exit` 並自動包裝 `try/finally`。鎖物件應為私有唯讀參考型別,避免 `lock(this)`、`lock(typeof(T))`、`lock("string")` 等反模式。
private static readonly object Locker = new object();
lock (Locker) {
// 臨界區程式碼
}
「做飯」案例:一口鍋(共享資源),熬粥與做魚兩執行緒協調。`AutoResetEvent` 釋放單一等待執行緒後自動重置;`ManualResetEvent` 需手動 `Reset`。
AutoResetEvent potFree = new AutoResetEvent(false); // 初始無鍋可用
// 熬粥執行緒
void Porridge() {
// 選材、淘米...
Thread.Sleep(2000); // 熬製
Console.WriteLine("粥好,鍋空了");
potFree.Set(); // 通知鍋可用
}
// 做魚執行緒
void MakeFish() {
// 洗魚、醃製...
potFree.WaitOne(); // 等待鍋空
// 烹調...
potFree.Set(); // 歸還鍋
}
用法類似 `Monitor`,但可命名供跨進程使用。進程內同步首選 `Monitor`,效能較好。
static Mutex mutex = new Mutex(); // 本地 Mutex
mutex.WaitOne();
try { /* 臨界區 */ } finally { mutex.ReleaseMutex(); }
允許多執行緒並發讀取,寫入時獨佔。適合快取、配置資料等場景。
ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(); Dictionarycache = new Dictionary (); public string Read(int key) { rwLock.EnterReadLock(); try { return cache[key]; } finally { rwLock.ExitReadLock(); } } public void Add(int key, string value) { rwLock.EnterWriteLock(); try { cache.Add(key, value); } finally { rwLock.ExitWriteLock(); } }
「購票」案例:3 個售票窗口,5 位顧客排隊。`Semaphore(0,3)` 初始 0 可用,主執行緒 `Release(2)` 開放 2 個窗口。
static Semaphore windows = new Semaphore(0, 3);
public void Pay(object id) {
windows.WaitOne(); // 佔用窗口
try { /* 結帳 */ } finally { windows.Release(); } // 釋放窗口
}
`SemaphoreSlim` 輕量、支援 `CancellationToken`、不支援跨進程,適合進程內高頻短等待。
所有參與者到達屏障後才能進入下一階段,可重複使用。
適用於極短臨界區(幾十指令)、多核低爭用。`SpinLock` 為值型別,需 `ref` 傳遞;`SpinWait` 封裝兩階段等待(先忙等待再核心等待)。
// SpinLock 範例
SpinLock sl = new SpinLock();
StringBuilder sb = new StringBuilder();
Action act = () => {
for (int i=0; i<100; i++) {
bool got = false;
try { sl.Enter(ref got); sb.Append(i); }
finally { if (got) sl.Exit(); }
}
};
Parallel.Invoke(act, act, act);
無萬用同步機制,需依據資源類型、爭用強度、跨進程需求、延遲敏感度選擇。原則:優先用高層抽象(`lock`、`Interlocked`、`Concurrent` 集合、`async/await`),僅在效能分析證實瓶頸時才降級至 `SpinLock` 等低階原語。