昨天專案冒出了一個狀況:後台程式發送了一個 Post 請求給 Auth 伺服器之後怎樣都等不到回傳結果,送完 Post 之後的下一行就是執行不到,而 Auth 伺服器那邊也沒有回報出任何執行錯誤,甚至用 Postman 打一樣的 API 過去明明就有正確回傳。我偵錯了兩三個小時就是找不出原因,但是偶然在查資料的時候瞄到了「deadlock」這個字我才恍然大悟是這麼回事,改了一下程式碼果然證明了是這樣沒錯。這個錯誤我很久就聽過了,不過真的還是要自己遇到一次才會比較印象深刻。
事後我找到了這樣的一個簡單範例濃縮了我的錯誤(C# 程式碼):
public ActionResult Action() {
var data = GetDataAsync().Result;
return View(data);
}
private async Task<string> GetDataAsync() {
var result = await MyWebService.GetDataAsync();
return result.ToString();
}
這樣的程式碼一跑下去就是死當在那邊給你看,而我原本的程式本質上就是有這樣的結構存在。這個會死的原因在於,預設情況中,內層的 await
會把 MyWebService.GetDataAsync()
的 callback(也就是 return result.ToString();
)排程到當前的 SynchronizationContext
裡面,它會等到 context 的 stack queue 清空了才被執行,問題是當前 context 正在等待外層的 GetDataAsync().Result
、而這個傢伙等的正是 callback,結果就是循環等待,deadlock!
那要怎麼解決呢?有兩個辦法。一個是改寫成:
var result = await MyWebService.GetDataAsync().ConfigureAwait(false);
這個 .ConfigureAwait(false)
的效果是不把 callback 放在當前的 context 而是預設 context(即 ThreadPool
之中),這樣一來 callback 就不會被擋住。另外一個辦法是不要在外層使用 task 的 .Result
或者 .Wait()
,而一律用 async
/await
的寫法,這樣也可以避免打架。
最好的習慣是兩者都用:寫 library 給別人用的時候盡量加上 .ConfigureAwait(false)
,而用別人的 library 的時候盡量固定用 async
/await
以免出錯。
留言