CodeGym /課程 /C# SELF /預防與排除 Deadlock

預防與排除 Deadlock

C# SELF
等級 57, 課堂 2
開放

1. 經典的 Coffman 條件

1971 年,電腦科學家 愛德華·科夫曼 (Edward G. Coffman Jr.) 描述了四個條件,若沒有這些條件就不可能發生 deadlock。這些規則已經變成多執行緒面試和課程的「基礎」。懂它們不只要背得起來,還要知道為什麼有用:

  1. 互斥 (Mutual Exclusion): 至少有一個資源一次只能被一個執行緒佔有。
  2. 佔有且等待 (Hold and Wait): 已佔有某個資源的執行緒可能會等待其他資源。
  3. 無法搶占 (No Preemption): 資源不能被強制從執行緒手上奪走 — 只有該執行緒自己能釋放它。
  4. 環狀等待 (Circular Wait): 存在這樣一條執行緒鍊,鏈上每個執行緒都在等待下一個執行緒持有的資源。

如果 這四個條件同時成立,deadlock 就有可能發生。只要破壞其中一個條件 — 互相等待的可能性就會消失。

快速決策流程

flowchart TD
    A[需要鎖住多個資源嗎?] -- 否 --> B[標準 lock]
    A -- 是 --> C[能不能永遠按同一個順序拿?]
    C -- 能 --> D[永遠按同一順序拿]
    C -- 不能 --> E[避免巢狀 lock]
    D --> F[把鎖住的時間最小化]
    E --> F
    B --> F
    F --> G{還是覺得害怕?}
    G -- 會 --> H[加上 timeout 並重試]
    G -- 不會 --> I[安心睡覺吧!]

2. 預防 Deadlock 的主要策略

既然 deadlock 是多執行緒世界的一部分,我們的任務是學會和它共存。不同策略可以幫忙,從簡單到進階都有。

永遠以同一順序鎖定資源

重點很簡單:如果所有執行緒永遠以相同的順序取得資源,那環狀等待就不可能發生。這招常被稱作 ordered locking — 「有序鎖定」。

範例

危險的程式碼:


// 執行緒 1
lock (resA)
{
    lock (resB)
    {
        // ...
    }
}

// 執行緒 2
lock (resB)
{
    lock (resA)
    {
        // ...
    }
}

在上面會發生 deadlock:第一個執行緒拿到 resA 然後在等 resB,第二個則相反。兩邊都卡住,永遠不會釋放。

正確的程式碼:


// 一定:先拿 resA,再拿 resB(絕不反過來)
lock (resA)
{
    lock (resB)
    {
        // ...
    }
}

現在順序一致,deadlock 就不可能發生。資源有多少、叫什麼名字不重要,重點是所有執行緒以相同順序拿鎖。

常見錯誤: 只要程式裡有一處打破了這個順序,deadlock 的風險就回來了。所以要特別小心!

避免同時鎖住多個資源

最簡單的做法就是:如果可以不要同時拿兩個以上的鎖,就不要拿!巢狀鎖越多,越容易被卡住。另一個做法是把需要的資料複製到暫存變數,先釋放鎖,然後再處理其他資源。

在取得鎖時使用 timeout

如果真的很需要同時拿幾個鎖,timeout 很有用。比如說,不使用單純的 lock,改用能「嘗試」取得鎖的方法,拿不到就把已拿的東西放回,像是 Monitor.TryEnter。


object resourceA = new object();
object resourceB = new object();

bool successA = false, successB = false;
try
{
    // 嘗試在最多 2 秒內拿到兩個鎖
    successA = Monitor.TryEnter(resourceA, TimeSpan.FromSeconds(2));
    if (!successA) return; // 拿不到就退出

    successB = Monitor.TryEnter(resourceB, TimeSpan.FromSeconds(2));
    if (!successB) return;

    // 臨界區
}
finally
{
    if (successB) Monitor.Exit(resourceB);
    if (successA) Monitor.Exit(resourceA);
}

如果第二個鎖在 2 秒內拿不到,我們就釋放第一個然後退出。這樣 deadlock 不會發生,最多只是某些執行緒會回退再嘗試。

把鎖住的程式碼範圍最小化

臨界區越小,其他執行緒等待的時間就越短。不應該在臨界區內做複雜運算、網路請求或磁碟 I/O。拿到鎖就盡快改變共用資源,然後放掉鎖。其他不相關的工作放到外面做。

拆分狀態 — 避免共用資源

有時候比起和鎖對抗,更好的策略是根本不要共享狀態:

  • 使用不可變物件 (immutable)。
  • 用複本或區域變數代替全域資料。
  • 以訊息傳遞為主(Actor Model、message queue)。
  • 使用 thread-safe 的集合:ConcurrentDictionary、ConcurrentQueue 等,來自 System.Collections.Concurrent。

3. 如果已經發生 Deadlock,怎麼處理

手動殺掉並重啟(不是最好的選擇)

最簡單的方式是把所有被卡住的 process 給結束掉。但這可能會造成資料遺失,屬於最後的手段,能避免就避免。

在執行中偵測 Deadlock

有些系統可以自動偵測 deadlock。如果一個執行緒拿鎖超過合理時間,可以把事件記 log、通知監控系統並啟動恢復流程。


if (!Monitor.TryEnter(resource, TimeSpan.FromSeconds(10)))
{
    Console.WriteLine("看起來我們可能進入了 deadlock,或是有人把鎖拿太久了!");
    // 可以啟動恢復、輸出 dump 或通知使用者
}

在分散式系統裡會用專門的偵測器去分析等待圖(wait-for graph),並強制回滾或解鎖僵住的交易。

自動回滾並重試

一個常用的做法是「認輸然後重試」:如果在合理時間內沒有拿到所有鎖,就回退、等一段時間(通常加上隨機抖動以避免衝突),然後再試。


for (int attempt = 0; attempt < 3; attempt++)
{
    bool gotRes1 = Monitor.TryEnter(res1, TimeSpan.FromSeconds(2));
    bool gotRes2 = false;
    try
    {
        if (gotRes1)
        {
            gotRes2 = Monitor.TryEnter(res2, TimeSpan.FromSeconds(2));
            if (gotRes2)
            {
                // 臨界區
                break;
            }
        }
    }
    finally
    {
        if (gotRes2) Monitor.Exit(res2);
        if (gotRes1) Monitor.Exit(res1);
    }

    // 不成功 — 等一下再試
    Thread.Sleep(500 + new Random().Next(500));
}

4. 最佳實務與建議

  • 分析鎖的取得順序。 聚合所有進入點並檢查順序是否一致。
  • 盡可能使用來自 System.Collections.Concurrent 的集合。
  • 使用程式碼分析工具。 IDE 像 Rider 或 Visual Studio 能幫忙找出巢狀鎖。
  • 記錄長時間的鎖取用嘗試。
  • 對多執行緒元件做壓力測試。
  • 文件化鎖順序的協議。 註解能防止違反有序性的情況。

5. 實務範例

範例:資料庫

在 RDBMS 裡 deadlock 很常見:兩個交易更新相同的表但順序不同(A→B 與 B→A)。大多數 DB 會偵測這類情況並 «kill» 掉其中一個交易。

範例:非同步方法

在 .NET 裡,如果把 async/await 和鎖混在一起可能會引發互相等待:await 會切換/讓出執行緒,而持有鎖的那端可能被阻塞,另外的執行緒又在等相同的資源。要規劃好臨界區的邊界,避免在裡面做 await 或其他會阻塞的等待。

6. 使用鎖時的常見錯誤

錯誤 #1:鎖字串。
lock (myString) — 這是壞主意。在 .NET 中字串會被 intern,所以你其實是在鎖一個全域的字串表。

錯誤 #2:不同地方用不同順序拿物件。
如果程式不同處以不同順序取得鎖,deadlock 的機率會大增。

錯誤 #3:鎖住太久。
把鎖持有時間拉得比需要的還長,或在臨界區內呼叫外部方法,會大幅增加出現 hang 或互相鎖住的風險。

2
任務
C# SELF, 等級 57, 課堂 2
上鎖
實現自動回滾以處理 Deadlock
實現自動回滾以處理 Deadlock
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION