Asynchronous Programming - 第五章:錯誤等待的死鎖之吻
他們曾經相愛,也曾經深信,只要彼此都願意等,愛就會回來。
她把訊息輸入對話框,卻沒有按下傳送,想等他主動說第一句話。她想:「他如果真的在乎,就會找我。」
他打開視窗看了又看,也沒傳訊息,心想:「她如果還有感覺,就會先聯絡我。」
他們每天打開彼此的聊天室又關上,不是沒有思念,而是不願先伸出手。怕輸、怕低頭、怕被拒絕。
於是他們靜靜地等,在各自的世界,畫了一個誰也跨不過的等待邊界。
這不是不愛,是兩個 .Result() 卡在彼此門前的靈魂,沒有人願意給對方 await 的空間。
🎵 問題緣起:為什麼 .Result 有時會讓整個程式卡住?
await 與 Task.Result/Task.Wait () 的 Deadlock 問題
在 ASP.NET 舊版框架(非 Core)中,如果你在同步方法中呼叫非同步方法,像這樣:
1 | var result = GetDataAsync().Result; |
你可能會遇到一個非常棘手的問題:死鎖(Deadlock)。
🎵 死鎖是怎麼發生的?
ASP.NET Framework 中的流程:
- 主執行緒呼叫 .Result,進入「同步等待」狀態,卡住不動。
- GetDataAsync() 裡面用 await,遇到像 await Task.Delay(…) 時,會中斷流程、把控制權讓出。
- 等待完成後,預設會「切回原本的執行緒」來繼續執行。
問題來了:原本的執行緒(主執行緒)正在 .Result 那裡等你。但同時,你要回去的執行緒被你自己卡住了 → 互相等待 → ❌ 死鎖。這一切的核心關鍵是:SynchronizationContext。它會記住「原本在哪條執行緒開始」,然後強迫 await 結束後一定要回來。
🎵 ASP.NET Core:不再死鎖
ASP.NET Core 拿掉了 SynchronizationContext,變成這樣:
“任務完成後,誰有空就接,不一定要回原來的執行緒。”
這意味著,await 後續不需要再等主執行緒空出來,所以就算主執行緒 .Result 卡在那,也不會阻止 await 的後續被別的執行緒執行,這樣就不會造成死鎖了。
🎵 那主執行緒還在等嗎?
是的,只要用了 .Result 或 .Wait(),主執行緒就還在等待任務完成。只是這次,它不再是唯一能「收尾」的人。
🧠 換句話說:主執行緒會等結果,但不是非得由它自己完成任務。
在 .NET Core 中,任務完成後可以由任意 ThreadPool 執行緒跑完 await 後續的邏輯,再把結果交回 .Result。
🎵 那如果我用 await 而不是 .Result 呢?
這是非同步程式的本質優勢,await 不會阻塞執行緒,它會把「後面要做的事」記下來(continuation),等有空的執行緒再來做,當下執行緒(例如主執行緒)就被「釋放」,可以去處理別的事,如此一來
少量執行緒也能處理大量請求 → 非同步的威力!
🎵 結語:有些等待,是互相讓步才能抵達的終點
他們明明都還放不下,卻又都太驕傲。你等我回應,我等你先問。於是我們都站在自己心門的另一端,渴望被理解,卻都不願先理解。非同步死鎖亦然。.Result 執著於原地等待,拒絕放手;await 想回到原點續寫後話,卻被拒於門外。
他們的故事不是結束於「沒有結果」,而是卡在「沒人讓步」。await 的哲學是這樣的:我相信你會完成,我暫時離開,但我有留下繼續的指引。這不是逃避,而是信任。有時我們在程式裡學到的等待之道,也該應用在人生。不是非得誰先低頭,而是先鬆開手,才能讓結果流動。讓愛,也讓錯誤,都有一條回來的路。