在编写异步方法时,使用 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 CPUint 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<T>?中導致死鎖 - 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
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結
以上是生活随笔為你收集整理的在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 8.0 中开启默认接口实现
- 下一篇: 求斐波那契数列第n位的几种实现方式及性能