死鎖的示例
下面就是一個會死鎖的示例代碼:
// 異步死鎖示例 - 不使用 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 等)時,這些線程被佔滿且不會很快釋放,新提交的任務只能在隊列裏等線程,就形成線程池飢餓:有活要幹,但沒有空閒線程來幹。