博客 / 詳情

返回

死鎖是怎麼發生的,舉個簡單的例子

死鎖的示例

下面就是一個會死鎖的示例代碼:

// 異步死鎖示例 - 不使用 TaskScheduler,僅用多個 Task 互相等待

Console.WriteLine("=== 多 Task 互相等待死鎖 ===\n");

// 兩個任務互相用 .Result 等待對方完成 → 死鎖
var tcsA = new TaskCompletionSource<int>();
var tcsB = new TaskCompletionSource<int>();

var taskA = Task.Run(() =>
{
    Console.WriteLine($"[任務 A] 開始,線程 {Environment.CurrentManagedThreadId}");
    Console.WriteLine("[任務 A] 等待任務 B 的結果...");

    int resultB = tcsB.Task.Result; // A 阻塞等待 B

    Console.WriteLine($"[任務 A] 收到 B 的結果: {resultB}");
    tcsA.SetResult(1);
});

var taskB = Task.Run(() =>
{
    Console.WriteLine($"[任務 B] 開始,線程 {Environment.CurrentManagedThreadId}");
    Console.WriteLine("[任務 B] 等待任務 A 的結果...");

    int resultA = tcsA.Task.Result; // B 阻塞等待 A

    Console.WriteLine($"[任務 B] 收到 A 的結果: {resultA}");
    tcsB.SetResult(2);
});

Console.WriteLine("等待所有任務完成(將在此處掛起)...\n");
Console.WriteLine("死鎖原因:");
Console.WriteLine("- 任務 A 用 .Result 阻塞等待 tcsB.Task 完成");
Console.WriteLine("- 任務 B 用 .Result 阻塞等待 tcsA.Task 完成");
Console.WriteLine("- tcsA.SetResult() 要等 A 等到 B 之後才會執行");
Console.WriteLine("- tcsB.SetResult() 要等 B 等到 A 之後才會執行");
Console.WriteLine("- A 等 B,B 等 A → 死鎖\n");

Task.WaitAll(taskA, taskB);

Console.WriteLine("若看到這行説明未發生死鎖(本示例中應看不到)");

任務 A: 阻塞在 tcsB.Task.Result → 等 B 完成
任務 B: 阻塞在 tcsA.Task.Result → 等 A 完成
              ↑_______________________________↓

  • A 要等到 tcsB.SetResult(2) 被調用才會從 .Result 返回
  • B 要等到 tcsA.SetResult(1) 被調用才會從 .Result 返回
  • 而 tcsA.SetResult(1) 在 A 裏、tcsB.SetResult(2) 在 B 裏,都要等對方先返回
  • 所以誰都不會先完成 → 死鎖

解決辦法

這個代碼運行起來肯定會死鎖,當然實際項目中死鎖的發生比這個要複雜,但是道理相似。
解決辦法就是把.Result這樣的阻塞寫法改成async await方式。

線程池飢餓

實際中可能是併發太高,線程池飢餓導致死鎖,也就是:線程池裏的工作線程數量有限。當大量任務都在阻塞等待(例如 .Result、.Wait()、Thread.Sleep()、lock 等)時,這些線程被佔滿且不會很快釋放,新提交的任務只能在隊列裏等線程,就形成線程池飢餓:有活要幹,但沒有空閒線程來幹。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.