昨天專案冒出了一個狀況:後台程式發送了一個 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 以免出錯。

分享此頁至:
最後修改日期: 2020/06/11

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。