歡迎光臨
每天分享高質量文章

在編寫非同步方法時,使用 ConfigureAwait(false) 避免使用者死鎖

我在 使用 Task.Wait()?立刻死鎖(deadlock) 一文中站在類庫使用者的角度看 async/await 程式碼的死鎖問題;而本文將站在類庫設計者的角度來看死鎖問題。

閱讀本文,我們將知道如何編寫類庫程式碼,來盡可能避免類庫使用者出現那篇部落格中描述的死鎖問題。


 

現在,我們是類庫設計者的身份,我們試圖編寫一個 RunAsync 方法用以非同步執行某些操作。

private async Task RunAsync()
{
    // 某些非同步操作。
}

類庫的使用者可能多種多樣,一個比較有素養的使用者會考慮這樣使用類庫:

放心,這樣的類庫使用者是不會出什麼岔子的。

然而,這世間既然有讓人省心的類庫使用者,當然也存在非常讓人不省心的類庫使用者。當你的類庫遍佈全球,你真的會遇到這樣的使用者:

或者高階一些,使用 AutoResetEvent 和 try/finally 塊的使用者:

// 這段程式碼如果在 foo.RunAsync() 第一次呼叫傳回之前再呼叫一次,則可能死鎖。
_autoResetEvent.WaitOne();
try
{
    await foo.RunAsync();
}
finally
{
    _autoResetEvent.Set();
}

如果這段程式碼在 UI 執行緒執行,那麼極有可能出現死鎖,就是我在 使用 Task.Wait()?立刻死鎖(deadlock) 一文中說的那種死鎖,詳情可進去看原因。

那麼現在做一個調查,你認為下麵三種 RunAsync 的實現中,哪些會在碰到這種不省心的類庫使用者時發生死鎖呢?

答案是——

第 2 種!

只有第 2 種會發生死鎖,第 1 和第 3 種都不會。

 

對於第 2 種情況,下方“await 之後的程式碼”試圖回到 UI 執行緒執行,但 UI 此時處於呼叫者 foo.RunAsync().Wait(); 這段神奇程式碼的等待狀態——所以死鎖了。回到 UI 執行緒靠的是 DispatcherSynchronizationContext,我在 使用 Task.Wait()?立刻死鎖(deadlock) 一文中已有解釋,建議前往瞭解更深層次的原因。

private async Task RunAsync1()
{
    await Task.Run(() =>
    {
        // 某些非同步操作。
    });
    // await 之後的程式碼(即使沒寫任何程式碼,也是需要執行的)。
}

那為什麼第 1 種和第 3 種不會死鎖呢?

對第 1 種情況,由於並沒有寫 async/await,所以非同步狀態機 AsyncMethodStateMachine 此時並不執行。直接傳回了 Task,這相當於此時建立的 Task 物件直接被呼叫者的 foo.RunAsync().Wait(); 神奇程式碼等待了。也就是說,等待的 Task 是真正執行非同步任務的 Task。

Task 的 Wait() 方法內部透過自旋鎖來實現等待,可以閱讀 .NET 中的輕量級執行緒安全 – walterlv 瞭解自旋鎖,也可以前往 .NET Framework 原始碼 Task.SpinWait 瞭解 Task.SpinWait() 方法的具體實現。

//spin only once if we are running on a single CPU
int spinCount = PlatformHelper.IsSingleProcessor
    ? 1
    : System.Threading.SpinWait.YIELD_THRESHOLD;
for (int i = 0; i < spinCount; i++)
{
    if (IsCompleted)
    {
        return true;
    }

    if (i == spinCount / 2)
    {
        Thread.Yield();
    }
    else
    {
        Thread.SpinWait(PlatformHelper.ProcessorCount * (4 << i));
    }
}

當 Run 中的非同步任務結束後,自旋鎖即發現任務結束 Task.IsCompleted 為 True,於是等待結束,不會發生死鎖。

對第 3 種情況,由於指定了 ConfigureAwait(false),這意味著通知非同步狀態機 AsyncMethodStateMachine 並不需要使用設定好的 SynchronizationContext(對於 UI 執行緒,是 DispatcherSynchronizationContext)執行執行緒同步,而是使用預設的 SynchronizationContext,而預設行為是隨便找個執行緒執行後面的程式碼。於是,await Task.Run 後面的程式碼便不需要傳回原執行緒,也就不會發生第 2 種情況裡的死鎖問題。

 

建議安裝 NuGet 包 Microsoft.CodeAnalysis.FxCopAnalyzers。這樣,當你在程式碼中寫出 await 時,分析器會提示你 CA2007 警告,你必須顯式設定 ConfigureAwait(false) 或 ConfigureAwait(true) 來提醒你是否需要使用預設的 SynchronizationContext。

如果你是類庫的編寫者,註意此問題能夠一定程度上防止逗比使用者出現死鎖問題後噴你的類庫寫得不好。

 

死鎖問題:

  • 使用 Task.Wait()?立刻死鎖(deadlock) – walterlv
  • 不要使用 Dispatcher.Invoke,因為它可能在你的延遲初始化 Lazy 中導致死鎖 – walterlv
  • 在有 UI 執行緒參與的同步鎖(如 AutoResetEvent)內部使用 await 可能導致死鎖
  • .NET 中小心巢狀等待的 Task,它可能會耗盡你執行緒池的現有資源,出現類似死鎖的情況 – walterlv

解決方法:

  • 在編寫非同步方法時,使用 ConfigureAwait(false) 避免使用者死鎖 – walterlv
  • 將 async/await 非同步程式碼轉換為安全的不會死鎖的同步程式碼(使用 PushFrame) – walterlv

原文地址:https://blog.walterlv.com/post/using-configure-await-to-avoid-deadlocks.html

已同步到看一看
贊(0)

分享創造快樂