首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# ManualResetEvent 與 AutoResetEvent 執行緒同步完整實戰指南

     編輯:關於C#

兩大事件等待控制項比較

特性ManualResetEventAutoResetEvent
信號狀態手動控制: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 等現代非同步原語,僅在需與舊式同步代碼互操作或極簡單場景下使用事件等待控制項。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved