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

C# 多執行緒同步化:lock 與 Monitor 實作生產者消費者模式完整教學

     編輯:關於C#

前言

多執行緒程式中,當多個執行緒同時存取共享資料 (如集合、變數、檔案) 時,若缺乏同步機制,會發生**競爭條件**,導致資料不一致、邏輯錯誤甚至程式當機。C# 提供了兩層級的同步原語:

  1. lock 關鍵字:語法糖,編譯器自動展開為 Monitor.Enter/Exit,適合**保護程式碼區塊** (Critical Section)。
  2. Monitor 類別:提供更精細的控制,如 Wait、Pulse、PulseAll,適合實作**執行緒間協調/通訊** (如生產者-消費者模式)。

本文將深入解析 lock 與 Monitor 的運作原理、最佳實踐,並以經典的**生產者-消費者模式**為例,完整示範如何使用 Monitor.Wait/Pulse 實現執行緒交替執行與同步通訊。

lock 關鍵字:保護關鍵區段

語法與原理

lock (expression) 
{
    // 關鍵區段:同一時間僅允許一個執行緒進入
}
  • expression:必須是**參考型別** 物件 (通常用 this、private readonly object _lock = new object()、或靜態類別的 typeof(MyClass))。
  • 編譯器展開為:
bool lockTaken = false;
try
{
    Monitor.Enter(lockObj, ref lockTaken);
    // 關鍵區段程式碼
}
finally
{
    if (lockTaken) Monitor.Exit(lockObj);
}

最佳實踐

經典範例:銀行帳戶提款 (避免負餘額)

class Account
{
    private int _balance;
    private readonly object _lock = new object();

    public Account(int initial) => _balance = initial;

    public int Withdraw(int amount)
    {
        lock (_lock) // 保護 _balance 讀寫的原子性
        {
            if (_balance >= amount)
            {
                Thread.Sleep(5); // 模擬處理延遲,若無 lock 會導致競爭
                _balance -= amount;
                return amount;
            }
            return 0; // 拒絕交易
        }
    }
}

Monitor 類別:執行緒協調與通訊

Monitor 是靜態類別,所有方法皆為靜態,需搭配**具體物件實例** 作為同步目標。核心方法:

原則說明
專用鎖物件宣告 private readonly object _lock = new object(),勿用 this、string、typeof()、公開欄位,防止外部程式碼惡意鎖定導致死結
鎖定範圍最小化僅包裹必要的共享資源存取程式碼,避免在 lock 內呼叫外部方法、I/O、Thread.Sleep
避免巢狀 lock若必須巢狀,嚴格遵守**固定取得順序** (如先 lock A 再 lock B),否則極易死結
不可重入同一執行緒可重複進入同一 lock (C# lock 可重入),但跨方法呼叫仍需注意邏輯一致性

物件內部三個隊列

  1. 擁有者:目前持有鎖的執行緒參考。
  2. 就緒隊列:已準備好取得鎖的執行緒 (被 Pulse 喚醒者)。
  3. 等待隊列:呼叫 Wait 釋放鎖並等待通知的執行緒。

流程:執行緒 Wait → 釋放鎖 + 進入等待隊列 → 他執行緒 Pulse → 移至就緒隊列 → 鎖釋放 → 就緒隊列執行緒取得鎖繼續執行。

完整範例:生產者-消費者模式 (Monitor.Wait/Pulse)

場景:一個緩衝區 (Cell),生產者寫入整數,消費者讀取並顯示。**嚴格交替**:生產一個 → 消費一個 → 生產下一個...

using System;
using System.Threading;

namespace ProducerConsumerDemo
{
    /// 
    /// 共享緩衝區:單一整數槽位,支援同步讀寫
    /// 
    class Cell
    {
        private int _cellContents;      // 緩衝資料
        private bool _readerFlag = false; // false = 可寫入/正在寫入, true = 可讀取/已寫入

        /// 
        /// 消費者呼叫:讀取資料 (若無資料則等待)
        /// 
        public int ReadFromCell()
        {
            lock (this) // 取得 Cell 物件鎖
            {
                // 若目前不可讀取 (無新資料),釋放鎖並等待 Pulse
                if (!_readerFlag)
                {
                    try
                    {
                        Monitor.Wait(this); // 釋放鎖,進入等待隊列
                    }
                    catch (SynchronizationLockException ex)
                    {
                        Console.WriteLine($"[Read] 同步鎖例外: {ex.Message}");
                    }
                    catch (ThreadInterruptedException ex)
                    {
                        Console.WriteLine($"[Read] 執行緒中斷: {ex.Message}");
                    }
                }

                // 被喚醒並重新取得鎖後,讀取資料
                Console.WriteLine($"消費: {_cellContents}");
                _readerFlag = false;      // 重置旗標:資料已被消費
                Monitor.Pulse(this);      // 通知生產者:可寫入下一筆
            }
            return _cellContents;
        }

        /// 
        /// 生產者呼叫:寫入資料 (若資料未被消費則等待)
        /// 
        public void WriteToCell(int n)
        {
            lock (this)
            {
                // 若目前不可寫入 (資料未被消費),釋放鎖並等待 Pulse
                if (_readerFlag)
                {
                    try
                    {
                        Monitor.Wait(this);
                    }
                    catch (SynchronizationLockException ex)
                    {
                        Console.WriteLine($"[Write] 同步鎖例外: {ex.Message}");
                    }
                    catch (ThreadInterruptedException ex)
                    {
                        Console.WriteLine($"[Write] 執行緒中斷: {ex.Message}");
                    }
                }

                // 被喚醒並重新取得鎖後,寫入資料
                _cellContents = n;
                Console.WriteLine($"生產: {_cellContents}");
                _readerFlag = true;       // 設定旗標:有新資料可讀
                Monitor.Pulse(this);      // 通知消費者:有新資料
            }
        }
    }

    /// 
    /// 生產者:連續寫入 1..quantity
    /// 
    class Producer
    {
        private readonly Cell _cell;
        private readonly int _quantity;

        public Producer(Cell cell, int quantity)
        {
            _cell = cell;
            _quantity = quantity;
        }

        public void Run()
        {
            for (int i = 1; i <= _quantity; i++)
                _cell.WriteToCell(i);
        }
    }

    /// 
    /// 消費者:連續讀取 quantity 次
    /// 
    class Consumer
    {
        private readonly Cell _cell;
        private readonly int _quantity;

        public Consumer(Cell cell, int quantity)
        {
            _cell = cell;
            _quantity = quantity;
        }

        public void Run()
        {
            for (int i = 1; i <= _quantity; i++)
                _cell.ReadFromCell();
        }
    }

    class Program
    {
        static void Main()
        {
            Console.WriteLine("=== 生產者-消費者模式 (Monitor.Wait/Pulse) ===");

            var cell = new Cell();
            var producer = new Producer(cell, 20);
            var consumer = new Consumer(cell, 20);

            // 建立兩個執行緒
            Thread producerThread = new Thread(producer.Run) { Name = "Producer" };
            Thread consumerThread = new Thread(consumer.Run) { Name = "Consumer" };

            try
            {
                producerThread.Start();
                consumerThread.Start();

                // 等待兩個執行緒結束
                producerThread.Join();
                consumerThread.Join();
            }
            catch (ThreadStateException ex)
            {
                Console.WriteLine($"執行緒狀態錯誤: {ex.Message}");
            }
            catch (ThreadInterruptedException ex)
            {
                Console.WriteLine($"執行緒被中斷: {ex.Message}");
            }

            Console.WriteLine("程式結束。");
            Console.ReadKey();
        }
    }
}

程式執行輸出

=== 生產者-消費者模式 (Monitor.Wait/Pulse) ===
生產: 1
消費: 1
生產: 2
消費: 2
生產: 3
消費: 3
...
生產: 20
消費: 20
程式結束。

流程詳解

  1. 生產者執行緒啟動,呼叫 WriteToCell(1) → 取得鎖 → _readerFlag=false 不等待 → 寫入 1 → _readerFlag=true → Pulse 喚醒消費者 → 釋放鎖。
  2. 消費者執行緒 (可能已在 ReadFromCell 中 Wait 等待) 被喚醒 → 重新取得鎖 → 讀取 1 → _readerFlag=false → Pulse 喚醒生產者 → 釋放鎖。
  3. 雙方以 _readerFlag 狀態旗標配合 Monitor.Wait/Pulse 實現**嚴格交替**,無忙碌等待、無資料遺漏、無競爭條件。

進階主題與現代替代方案

1. 緩衝區擴充:BlockingCollection<T> (推薦 .NET 4.0+)

System.Collections.Concurrent.BlockingCollection<T> 內建**有界緩衝區**、**生產者阻塞**、**消費者阻塞**、**完成標記**,完全取代手寫 Monitor 邏輯:

var buffer = new BlockingCollection(boundedCapacity: 10);

// 生產者
Task.Run(() => {
    for (int i = 1; i <= 20; i++) buffer.Add(i); // 滿時自動阻塞
    buffer.CompleteAdding(); // 標記完成
});

// 消費者
Task.Run(() => {
    foreach (var item in buffer.GetConsumingEnumerable()) // 空時自動阻塞,CompleteAdding 後結束
        Console.WriteLine($"消費: {item}");
});

2. 信號量 SemaphoreSlim:控制並存取數量

若允許 **N 個執行緒同時存取** 資源 (如連線池、並發限制),使用 SemaphoreSlim:

var sem = new SemaphoreSlim(3); // 最多 3 個並行
await sem.WaitAsync();
try { /* 存取資源 */ }
finally { sem.Release(); }

3. 讀寫鎖 ReaderWriterLockSlim:多讀單寫

資源「讀多寫少」時,ReaderWriterLockSlim 允許**多個讀取者並行**,寫入時**獨佔**:

var rwLock = new ReaderWriterLockSlim();
// 讀取
rwLock.EnterReadLock(); try { /* 讀取 */ } finally { rwLock.ExitReadLock(); }
// 寫入
rwLock.EnterWriteLock(); try { /* 寫入 */ } finally { rwLock.ExitWriteLock(); }

4. 非同步協調:TaskCompletionSource / Channel<T>

現代 .NET (Core/.NET 5+) 推薦使用 System.Threading.Channels.Channel<T> 實作高效能生產者-消費者:

var channel = Channel.CreateBounded(new BoundedChannelOptions(100) 
{ 
    FullMode = BoundedChannelFullMode.Wait 
});

// 生產者
await channel.Writer.WriteAsync(item);

// 消費者
await foreach (var item in channel.Reader.ReadAllAsync())
{ /* 處理 */ }

總結

lock 是保護關鍵區段最簡潔、安全的語法糖;Monitor 提供 Wait/Pulse/PulseAll 實現執行緒間**精細協調與通訊**,是實作生產者-消費者、讀寫鎖、條件變數等進階同步模式的基石。理解 Monitor.Wait **釋放鎖並等待**、Pulse **喚醒單一等待者** 的機制,是掌握多執行緒同步的關鍵。實務開發中,優先使用 BlockingCollection、Channel<T>、SemaphoreSlim、ReaderWriterLockSlim 等高階並發集合與同步原語,減少手寫 Monitor 邏輯帶來的錯誤風險。核心原則:**鎖定範圍最小化、專用鎖物件、避免巢狀死結、善用現代高階抽象**。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
方法用途
Enter(obj) / TryEnter(obj)取得物件鎖 (同 lock)
Exit(obj)釋放物件鎖
Wait(obj)**釋放鎖並封鎖目前執行緒**,進入物件的**等待隊列**,直到被 Pulse/PulseAll 喚醒並重新取得鎖
Pulse(obj)喚醒等待隊列中**第一個**執行緒,移至**就緒隊列**
PulseAll(obj)喚醒等待隊列中**所有**執行緒