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

C# 多線程機制初探(四):lock 與 Monitor 同步機制詳解

     編輯:C# 入門

前言

在多線程程式設計中,確保共享資料的安全存取是核心課題。C# 提供了 lock 關鍵字與 System.Threading.Monitor 類別兩種主要同步機制,本文將深入解析其用法、差異與實際應用場景。

一、lock 關鍵字

lock 是 C# 內建的語法糖,編譯後會轉換為 Monitor.Enter 與 Monitor.Exit 呼叫,並透過 try/finally 確保鎖定一定會被釋放。

基本語法

lock (expression)
{
    statement_block
}
  • expression:欲鎖定的物件參考,通常使用 this 保護實例成員,或類別名稱保護靜態成員。
  • statement_block:互斥區塊,同一時間僅允許一個執行緒進入。

經典範例:銀行帳戶提款

using System;
using System.Threading;

internal class Account
{
    private int balance;
    private Random r = new Random();

    internal Account(int initial) => balance = initial;

    internal int Withdraw(int amount)
    {
        if (balance < 0)
            throw new Exception("Negative Balance");

        lock (this)
        {
            Console.WriteLine($"Current Thread: {Thread.CurrentThread.Name}");
            if (balance >= amount)
            {
                Thread.Sleep(5); // 模擬耗時操作
                balance -= amount;
                return amount;
            }
            return 0; // 交易拒絕
        }
    }

    internal void DoTransactions()
    {
        for (int i = 0; i < 100; i++)
            Withdraw(r.Next(-50, 100));
    }
}

internal class Test
{
    static Thread[] threads = new Thread[10];

    public static void Main()
    {
        Account acc = new Account(0);
        for (int i = 0; i < 10; i++)
        {
            Thread t = new Thread(new ThreadStart(acc.DoTransactions));
            threads[i] = t;
            threads[i].Name = i.ToString();
        }
        for (int i = 0; i < 10; i++)
            threads[i].Start();

        Console.ReadLine();
    }
}

此範例展示若無 lock 保護,多執行緒同時檢查 balance >= amount 後皆執行扣款,導致餘額可能變為負值。加上 lock 後,檢查與扣款成為原子操作,確保資料一致性。

二、Monitor 類別

當多執行緒共用同一物件(而非共用程式碼區塊)時,Monitor 提供更細粒度的控制。它是靜態類別,所有方法皆為靜態,無法實例化。

核心方法

  • Monitor.Enter(object obj):取得物件鎖定,若已被其他執行緒持有則封鎖等待。
  • Monitor.Exit(object obj):釋放鎖定,建議置於 finally 區塊確保釋放。
  • Monitor.Pulse(object obj):通知等待佇列中第一個執行緒,將其移至準備佇列。
  • Monitor.Wait(object obj):釋放鎖定並進入等待佇列,直到收到 Pulse 或 PulseAll。

物件鎖定機制

每個被 Monitor 鎖定的物件在記憶體中維護三項資訊:

  1. 持有鎖定的執行緒參考
  2. 準備佇列:已準備好取得鎖定的執行緒
  3. 等待佇列:正在等待物件狀態變更的執行緒

當持有鎖定的執行緒呼叫 Pulse,等待佇列首位執行緒移至準備佇列;鎖定釋放時,準備佇列中的執行緒立即取得鎖定。

基本用法範例

Queue oQueue = new Queue();
// ...
Monitor.Enter(oQueue);
try
{
    // 此處 oQueue 只能被當前執行緒操作
}
finally
{
    Monitor.Exit(oQueue); // 確保釋放鎖定
}

三、生產者-消費者模式完整範例

結合 lock 與 Monitor.Wait/Pulse 實作經典的執行緒同步與通訊:

using System;
using System.Threading;

public class ProducerConsumer
{
    private readonly object _lock = new object();
    private int _data = -1;
    private bool _hasData = false;

    public void Produce(int value)
    {
        lock (_lock)
        {
            while (_hasData)
                Monitor.Wait(_lock); // 等待消費者取走

            _data = value;
            _hasData = true;
            Console.WriteLine($"Produced: {value}");
            Monitor.Pulse(_lock); // 通知消費者
        }
    }

    public int Consume()
    {
        lock (_lock)
        {
            while (!_hasData)
                Monitor.Wait(_lock); // 等待生產者放入

            int value = _data;
            _hasData = false;
            Console.WriteLine($"Consumed: {value}");
            Monitor.Pulse(_lock); // 通知生產者
            return value;
        }
    }
}

// 使用範例
public class Test
{
    static void Main()
    {
        var pc = new ProducerConsumer();
        var producer = new Thread(() => {
            for (int i = 0; i < 5; i++) pc.Produce(i);
        });
        var consumer = new Thread(() => {
            for (int i = 0; i < 5; i++) pc.Consume();
        });
        producer.Start();
        consumer.Start();
        producer.Join();
        consumer.Join();
    }
}

此範例展示生產者與消費者交替執行,透過 Monitor.Wait 釋放鎖定並等待,Monitor.Pulse 喚醒對方,達成高效協作。

四、總結與最佳實務

特性lockMonitor
易用性語法簡潔,自動處理 try/finally需手動管理 Enter/Exit,彈性較高
適用場景簡單的互斥同步需等待/通知機制、超時控制、多條件變數
效能相同(lock 編譯為 Monitor)相同
死鎖風險較低(區塊結構)較高(需自行確保釋放)
  • 優先使用 lock 處理單純互斥需求,程式碼更清晰安全。
  • 需跨執行緒等待/通知(如生產者-消費者)時,改用 Monitor.Wait/Pulse。
  • 避免鎖定 this、typeof(Type)、字串常數等公開可見物件,建議使用私有 readonly object _lock = new object();。
  • 鎖定區塊應盡可能精簡,避免在鎖定內呼叫外部方法或執行耗時 I/O。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved