兩大事件等待控制項比較
| 特性 | ManualResetEvent | AutoResetEvent |
| 信號狀態 | 手動控制:Set() 開啟,Reset() 關閉,維持直到下次 Reset | 自動控制:Set() 開啟,釋放**單一**等待執行緒後自動關閉 |
| 適用場景 | 廣播通知:一個事件通知多個執行緒(如「資料已更新,所有讀取者可繼續」) | 互斥/輪詢:多個執行緒搶單一工作項目(如工作佇列消費者) |
| 典型模式 | 閘門 | 信號燈 |
核心方法
WaitOne():無限期阻塞直到收到信號。
WaitOne(timeout):指定毫秒逾時,回傳 bool 是否收到信號。
Set():發送信號(開啟閘門/亮綠燈)。
Reset():ManualResetEvent 專用,關閉閘門(紅燈)。
實戰範例:一寫多讀 + 優雅關閉
using System;
using System.Threading;
public class DataCache
{
private readonly ManualResetEvent _dataReady = new ManualResetEvent(true); // 初始開啟
private readonly ManualResetEvent _shutdown = new ManualResetEvent(false);
private string _data = "初始資料";
private int _version = 0;
public string Read()
{
// 等待資料就緒(寫入時會暫時關閉)
_dataReady.WaitOne();
return _data;
}
public void Write(string newData)
{
// 1. 關閉閘門:阻塞所有新的讀取者
_dataReady.Reset();
// 2. 模擬耗時更新
Thread.Sleep(2000);
_data = newData;
_version++;
Console.WriteLine($"[Writer] 更新完成 v{_version}");
// 3. 開啟閘門:喚醒所有等待的讀取者
_dataReady.Set();
}
public void StartReaderLoop(int id)
{
new Thread(() =>
{
while (!_shutdown.WaitOne(10)) // 每 10ms 檢查關閉信號
{
string data = Read();
Console.WriteLine($"[Reader{id}] 讀取: {data} v{_version}");
Thread.Sleep(50); // 模擬處理時間
}
Console.WriteLine($"[Reader{id}] 已停止");
}) { IsBackground = true }.Start();
}
public void Shutdown()
{
_shutdown.Set(); // 廣播關閉信號
}
}
// 使用
var cache = new DataCache();
cache.StartReaderLoop(1);
cache.StartReaderLoop(2);
Thread.Sleep(100);
cache.Write("新資料 A");
Thread.Sleep(3000);
cache.Write("新資料 B");
Thread.Sleep(1000);
cache.Shutdown();
Thread.Sleep(500); // 等待讀取執行緒結束
進階模式:生產者-消費者佇列
public class WorkQueue
{
private readonly Queue _queue = new Queue();
private readonly AutoResetEvent _workAvailable = new AutoResetEvent(false);
private readonly ManualResetEvent _shutdown = new ManualResetEvent(false);
public void Enqueue(T item)
{
lock (_queue) _queue.Enqueue(item);
_workAvailable.Set(); // 喚醒**一個**消費者
}
public bool TryDequeue(out T item, int timeoutMs = -1)
{
// 等待工作或關閉信號
int index = WaitHandle.WaitAny(new[] { _workAvailable, _shutdown }, timeoutMs);
if (index == 1) { item = default; return false; } // 關閉信號
lock (_queue)
{
if (_queue.Count == 0) { item = default; return false; } // 假喚醒
item = _queue.Dequeue();
return true;
}
}
public void Shutdown() => _shutdown.Set();
}
WaitHandle.WaitAll / WaitAny 多重等待
// 同時等待多個條件(邏輯 AND)
WaitHandle.WaitAll(new[] { evt1, evt2 }); // 兩者都收到信號才繼續
// 等待任一條件(邏輯 OR)
int idx = WaitHandle.WaitAny(new[] { evt1, evt2, shutdown });
// idx = 0/1/2 表示哪個事件被觸發
常見錯誤與最佳實務
| 錯誤 | 後果 | 正確做法 |
忘記 Reset() (ManualResetEvent) | 閘門長期開啟,寫入期間讀取者讀到髒資料 | 寫入前必須 Reset(),寫完 Set() |
| 用 AutoResetEvent 做廣播 | 只喚醒一個執行緒,其餘永久阻塞 | 廣播用 ManualResetEvent |
| 無限期 WaitOne() 無關閉機制 | 程序無法優雅退出,執行緒洩漏 | 配合 CancellationToken 或額外 ManualResetEvent 做關閉信號 |
| 未處理假喚醒 | AutoResetEvent 偶爾會無信號喚醒 | 迴圈檢查條件,如佇列 Count > 0 |
| 大量執行緒 WaitOne 同一 AutoResetEvent | 驚群效應,上下文切換開銷大 | 考慮 SemaphoreSlim / Channel / BlockingCollection |
現代替代方案
- SemaphoreSlim:輕量非同步信號量,支援
WaitAsync,適合非同步流程控制。
- System.Threading.Channels:.NET Core 內建生產者-消費者通道,無鎖、支援背壓、非同步完美整合。
- BlockingCollection:封裝佇列 + 同步,簡單易用。
- async/await + TaskCompletionSource:事件式非同步等待,無阻塞執行緒。
總結
ManualResetEvent 適合「一對多」廣播同步(閘門),AutoResetEvent 適合「多對一」工作分發(信號燈)。實務上務必搭配關閉信號實現優雅退出,並注意假喚醒與驚群效應。新專案優先考慮 Channels、SemaphoreSlim 等現代非同步原語,僅在需與舊式同步代碼互操作或極簡單場景下使用事件等待控制項。