在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁
我在?使用 Task.Wait()?立刻死鎖(deadlock)?一文中站在類庫使用者的角度看?async/await?代碼的死鎖問題;而本文將站在類庫設(shè)計(jì)者的角度來看死鎖問題。
閱讀本文,我們將知道如何編寫類庫代碼,來盡可能避免類庫使用者出現(xiàn)那篇博客中描述的死鎖問題。
現(xiàn)在,我們是類庫設(shè)計(jì)者的身份,我們?cè)噲D編寫一個(gè)?RunAsync?方法用以異步執(zhí)行某些操作。
private async Task RunAsync(){
// 某些異步操作。
}
類庫的使用者可能多種多樣,一個(gè)比較有素養(yǎng)的使用者會(huì)考慮這樣使用類庫:
放心,這樣的類庫使用者是不會(huì)出什么岔子的。
然而,這世間既然有讓人省心的類庫使用者,當(dāng)然也存在非常讓人不省心的類庫使用者。當(dāng)你的類庫遍布全球,你真的會(huì)遇到這樣的使用者:
或者高級(jí)一些,使用?AutoResetEvent?和?try/finally?塊的使用者:
// 這段代碼如果在 foo.RunAsync() 第一次調(diào)用返回之前再調(diào)用一次,則可能死鎖。_autoResetEvent.WaitOne();
try
{
await foo.RunAsync();
}
finally
{
_autoResetEvent.Set();
}
如果這段代碼在 UI 線程執(zhí)行,那么極有可能出現(xiàn)死鎖,就是我在?使用 Task.Wait()?立刻死鎖(deadlock)?一文中說的那種死鎖,詳情可進(jìn)去看原因。
那么現(xiàn)在做一個(gè)調(diào)查,你認(rèn)為下面三種?RunAsync?的實(shí)現(xiàn)中,哪些會(huì)在碰到這種不省心的類庫使用者時(shí)發(fā)生死鎖呢?
答案是——
第 2 種!
只有第 2 種會(huì)發(fā)生死鎖,第 1 和第 3 種都不會(huì)。
對(duì)于第 2 種情況,下方“await?之后的代碼”試圖回到 UI 線程執(zhí)行,但 UI 此時(shí)處于調(diào)用者?foo.RunAsync().Wait();?這段神奇代碼的等待狀態(tài)——所以死鎖了。回到 UI 線程靠的是?DispatcherSynchronizationContext,我在?使用 Task.Wait()?立刻死鎖(deadlock)?一文中已有解釋,建議前往了解更深層次的原因。
private async Task RunAsync1(){
await Task.Run(() =>
{
// 某些異步操作。
});
// await 之后的代碼(即使沒寫任何代碼,也是需要執(zhí)行的)。
}
那為什么第 1 種和第 3 種不會(huì)死鎖呢?
對(duì)第 1 種情況,由于并沒有寫?async/await,所以異步狀態(tài)機(jī)?AsyncMethodStateMachine?此時(shí)并不執(zhí)行。直接返回了?Task,這相當(dāng)于此時(shí)創(chuàng)建的?Task?對(duì)象直接被調(diào)用者的?foo.RunAsync().Wait();?神奇代碼等待了。也就是說,等待的?Task?是真正執(zhí)行異步任務(wù)的?Task。
Task?的?Wait()?方法內(nèi)部通過自旋鎖來實(shí)現(xiàn)等待,可以閱讀?.NET 中的輕量級(jí)線程安全 - walterlv?了解自旋鎖,也可以前往 .NET Framework 源碼?Task.SpinWait?了解?Task.SpinWait()?方法的具體實(shí)現(xiàn)。
//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));
}
}
當(dāng)?Run?中的異步任務(wù)結(jié)束后,自旋鎖即發(fā)現(xiàn)任務(wù)結(jié)束?Task.IsCompleted?為?True,于是等待結(jié)束,不會(huì)發(fā)生死鎖。
對(duì)第 3 種情況,由于指定了?ConfigureAwait(false),這意味著通知異步狀態(tài)機(jī)?AsyncMethodStateMachine?并不需要使用設(shè)置好的?SynchronizationContext(對(duì)于 UI 線程,是?DispatcherSynchronizationContext)執(zhí)行線程同步,而是使用默認(rèn)的?SynchronizationContext,而默認(rèn)行為是隨便找個(gè)線程執(zhí)行后面的代碼。于是,await Task.Run?后面的代碼便不需要返回原線程,也就不會(huì)發(fā)生第 2 種情況里的死鎖問題。
建議安裝 NuGet 包?Microsoft.CodeAnalysis.FxCopAnalyzers。這樣,當(dāng)你在代碼中寫出?await?時(shí),分析器會(huì)提示你?CA2007?警告,你必須顯式設(shè)置?ConfigureAwait(false)?或?ConfigureAwait(true)?來提醒你是否需要使用默認(rèn)的?SynchronizationContext。
如果你是類庫的編寫者,注意此問題能夠一定程度上防止逗比使用者出現(xiàn)死鎖問題后噴你的類庫寫得不好。
死鎖問題:
使用 Task.Wait()?立刻死鎖(deadlock) - walterlv
不要使用 Dispatcher.Invoke,因?yàn)樗赡茉谀愕难舆t初始化?Lazy<T>?中導(dǎo)致死鎖 - walterlv
在有 UI 線程參與的同步鎖(如 AutoResetEvent)內(nèi)部使用 await 可能導(dǎo)致死鎖
.NET 中小心嵌套等待的 Task,它可能會(huì)耗盡你線程池的現(xiàn)有資源,出現(xiàn)類似死鎖的情況 - walterlv
解決方法:
在編寫異步方法時(shí),使用 ConfigureAwait(false) 避免使用者死鎖 - walterlv
將 async/await 異步代碼轉(zhuǎn)換為安全的不會(huì)死鎖的同步代碼(使用 PushFrame) - walterlv
原文地址:https://blog.walterlv.com/post/using-configure-await-to-avoid-deadlocks.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號(hào)文章匯總?http://www.csharpkit.com?
總結(jié)
以上是生活随笔為你收集整理的在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 8.0 中开启默认接口实现
- 下一篇: 求斐波那契数列第n位的几种实现方式及性能