多數死鎖源於多執行緒以不一致順序取得相同資源鎖。經典範例:轉帳操作需同時鎖定「轉出帳戶」與「轉入帳戶」。
public void Transfer(Account from, Account to, decimal amount)
{
Monitor.Enter(from); // 執行緒 A 先鎖 from,執行緒 B 先鎖 to
Monitor.Enter(to); // 互相等待對方釋放 → 死鎖
try { /* 業務邏輯 */ }
finally
{
Monitor.Exit(from);
Monitor.Exit(to);
}
}
當執行緒 A 執行 Transfer(A, B, 100)、執行緒 B 執行 Transfer(B, A, 50) 時,雙方各持一鎖、各等一鎖,形成循環等待。
為所有可被鎖定的物件建立固定排序規則(如依物件 ID、記憶體位址、或固定欄位),所有執行緒嚴格遵循此順序取得鎖。
public void Transfer(Account from, Account to, decimal amount)
{
// 依 ID 大小決定鎖順序,確保全域一致
Account first = from.Id < to.Id ? from : to;
Account second = from.Id < to.Id ? to : from;
Monitor.Enter(first);
try
{
Monitor.Enter(second);
try
{
// 業務邏輯:扣款、存入
from.Balance -= amount;
to.Balance += amount;
}
finally { Monitor.Exit(second); }
}
finally { Monitor.Exit(first); }
}
若業務允許,可用單一靜態物件或專用鎖物件保護整個關鍵區段,犧牲平行度換取簡單與安全。
private static readonly object _transferLock = new object();
public void Transfer(Account from, Account to, decimal amount)
{
lock (_transferLock) // 同一時間僅允許一筆轉帳
{
from.Balance -= amount;
to.Balance += amount;
}
}
| 策略 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 統一鎖順序 | 保留細粒度平行度 | 需嚴格規範、程式碼較繁 | 高併發、資源爭用明確 |
| 單一全域鎖 | 程式碼極簡、零死鎖風險 | 嚴重限制平行度 | 低併發、操作極快、或暫時方案 |
public bool TryTransfer(Account from, Account to, decimal amount, int timeoutMs = 1000)
{
Account first = from.Id < to.Id ? from : to;
Account second = from.Id < to.Id ? to : from;
if (Monitor.TryEnter(first, timeoutMs))
{
try
{
if (Monitor.TryEnter(second, timeoutMs))
{
try
{
from.Balance -= amount;
to.Balance += amount;
return true;
}
finally { Monitor.Exit(second); }
}
}
finally { Monitor.Exit(first); }
}
return false; // 取得鎖超時,避免死鎖
}
lock 區塊內只放必要邏輯,避免 I/O、呼叫外部方法SemaphoreSlim、ReaderWriterLockSlim、ConcurrentDictionary 等Interlocked、System.Threading.Channels、Immutable CollectionsMonitor.TryEnter 超時機制,記錄日誌便事後分析死鎖本質是「循環等待」,核心對策是打破循環等待條件。最穩健的做法是架構層面規範鎖順序,搭配 TryEnter 超時保護;對於低爭用場景,單一全域鎖亦可接受。切記:lock(this) 或 lock(typeof(Type)) 易被外部干擾,建議改用 private readonly object _lock = new object() 專用鎖物件。