在多線程程式設計中,確保共享資料的安全存取是核心課題。C# 提供了 lock 關鍵字與 System.Threading.Monitor 類別兩種主要同步機制,本文將深入解析其用法、差異與實際應用場景。
lock 是 C# 內建的語法糖,編譯後會轉換為 Monitor.Enter 與 Monitor.Exit 呼叫,並透過 try/finally 確保鎖定一定會被釋放。
lock (expression)
{
statement_block
}
this 保護實例成員,或類別名稱保護靜態成員。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.Enter(object obj):取得物件鎖定,若已被其他執行緒持有則封鎖等待。Monitor.Exit(object obj):釋放鎖定,建議置於 finally 區塊確保釋放。Monitor.Pulse(object obj):通知等待佇列中第一個執行緒,將其移至準備佇列。Monitor.Wait(object obj):釋放鎖定並進入等待佇列,直到收到 Pulse 或 PulseAll。每個被 Monitor 鎖定的物件在記憶體中維護三項資訊:
當持有鎖定的執行緒呼叫 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 喚醒對方,達成高效協作。
| 特性 | lock | Monitor |
|---|---|---|
| 易用性 | 語法簡潔,自動處理 try/finally | 需手動管理 Enter/Exit,彈性較高 |
| 適用場景 | 簡單的互斥同步 | 需等待/通知機制、超時控制、多條件變數 |
| 效能 | 相同(lock 編譯為 Monitor) | 相同 |
| 死鎖風險 | 較低(區塊結構) | 較高(需自行確保釋放) |
lock 處理單純互斥需求,程式碼更清晰安全。Monitor.Wait/Pulse。this、typeof(Type)、字串常數等公開可見物件,建議使用私有 readonly object _lock = new object();。