C# 为什么我需要在所有可传递闭包中使用ConfigureAwait(false)?

C# 为什么我需要在所有可传递闭包中使用ConfigureAwait(false)?,c#,multithreading,asynchronous,C#,Multithreading,Asynchronous,在阅读本文之后,我正在学习async/Wait 还有这个 我注意到@Stephen Cleary的文章中有一条提示 使用ConfigureWait(false)避免死锁是一种危险的做法。对于阻塞代码调用的所有方法(包括所有第三方和第二方代码)的传递闭包中的每个等待,都必须使用ConfigureAwait(false)。使用ConfigureAwait(false)来避免死锁充其量只是一种攻击) 它再次出现在我上面所附的邮政编码中 public async Task<HtmlDocument

在阅读本文之后,我正在学习async/Wait

还有这个

我注意到@Stephen Cleary的文章中有一条提示

使用ConfigureWait(false)避免死锁是一种危险的做法。对于阻塞代码调用的所有方法(包括所有第三方和第二方代码)的传递闭包中的每个等待,都必须使用ConfigureAwait(false)。使用ConfigureAwait(false)来避免死锁充其量只是一种攻击)

它再次出现在我上面所附的邮政编码中

public async Task<HtmlDocument> LoadPage(Uri address)
{
    using (var httpResponse = await new HttpClient().GetAsync(address)
        .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
    using (var responseContent = httpResponse.Content)
    using (var contentStream = await responseContent.ReadAsStreamAsync()
        .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
        return LoadHtmlDocument(contentStream); //CPU-bound
}
公共异步任务加载页(Uri地址)
{
使用(var httpResponse=wait new HttpClient().GetAsync(地址)
.ConfigureAwait(continueOnCapturedContext:false))//IO绑定
使用(var responseContent=httpResponse.Content)
使用(var contentStream=await responseContent.ReadAsStreamAsync()
.ConfigureAwait(continueOnCapturedContext:false))//IO绑定
返回LoadHtmlDocument(contentStream);//CPU绑定
}
据我所知,当我们使用ConfigureAwait(false)时,异步方法的其余部分将在线程池中运行。为什么我们需要将它添加到每个等待中 在传递闭包中?我自己认为这是我所知道的正确版本

public async Task<HtmlDocument> LoadPage(Uri address)
{
    using (var httpResponse = await new HttpClient().GetAsync(address)
        .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
    using (var responseContent = httpResponse.Content)
    using (var contentStream = await responseContent.ReadAsStreamAsync()) //IO-bound
        return LoadHtmlDocument(contentStream); //CPU-bound
}
公共异步任务加载页(Uri地址)
{
使用(var httpResponse=wait new HttpClient().GetAsync(地址)
.ConfigureAwait(continueOnCapturedContext:false))//IO绑定
使用(var responseContent=httpResponse.Content)
使用(var contentStream=await responseContent.ReadAsStreamAsync())//IO绑定
返回LoadHtmlDocument(contentStream);//CPU绑定
}
这意味着在使用块时第二次使用ConfigureAwait(false)是无用的。请告诉我正确的方法。 提前谢谢

据我所知,当我们使用
ConfigureAwait(false)
时,异步方法的其余部分将在线程池中运行

很接近,但有一个重要的警告,你错过了。使用
ConfigureAwait(false)
等待任务后继续时,将在任意线程上继续。记下“当你恢复时”这几个字

让我给你看看:

公共异步任务GetValueAsync() { 返回“缓存值”; } 公共异步任务示例1() { 等待此消息。GetValueAsync().ConfigureAwait(false); } 考虑
Example1
中的
wait
。尽管您正在等待一个
async
方法,但该方法实际上并不执行任何异步工作。如果一个
async
方法没有
wait
任何东西,它将同步执行,而waiter永远不会恢复,因为它从一开始就没有挂起过。如本例所示,对
ConfigureAwait(false)
的调用可能是多余的:它们可能根本没有效果。在本例中,当您输入
Example1
时所处的任何上下文都是在
wait
之后所处的上下文

和你想象的不太一样,对吧?然而,这并不完全是不寻常的。许多
async
方法可能包含不需要调用方挂起的快速路径。缓存资源的可用性就是一个很好的例子(谢谢@jakub-dąbek!),但是
异步
方法可能会过早退出。我们经常在方法开始时检查各种条件,看看是否可以避免做不必要的工作,
async
方法也一样

让我们看另一个例子,这次来自WPF应用程序:

async Task DoSomethingBenignAsync()
{
    await Task.Yield();
}

Task DoSomethingUnexpectedAsync()
{
    var tcs = new TaskCompletionSource<string>();
    Dispatcher.BeginInvoke(Action(() => tcs.SetResult("Done!")));
    return tcs.Task;
}

async Task Example2()
{
    await DoSomethingBenignAsync().ConfigureAwait(false);
    await DoSomethingUnexpectedAsync();
}
异步任务DoSomethingBenignAsync()
{
等待任务;
}
任务doSomethinguneExpectedAsync()
{
var tcs=new TaskCompletionSource();
Dispatcher.BeginInvoke(操作(()=>tcs.SetResult(“完成”));
返回tcs.Task;
}
异步任务示例2()
{
wait DoSomethingBenignAsync().configurewait(false);
等待dosomethinguneExpectedAsync();
}
请看
示例2
。我们等待的第一个方法总是异步运行。当我们点击第二个
await
时,我们知道我们正在一个线程池线程上运行,所以在第二个调用中不需要
ConfigureAwait(false)
,对吗?错。尽管名称中有
Async
并返回
任务
,但我们的第二个方法并不是使用
Async
wait
编写的。相反,它执行自己的调度,并使用
TaskCompletionSource
传递结果。当您从
wait
恢复时,您可能会[1]在提供结果的任何线程上运行,在本例中,该线程是WPF的dispatcher线程。哎呀

这里的关键是,您通常不知道“等待”方法的确切功能。无论是否使用
configurewait
,您都可能会在意外的地方运行。这可能发生在
async
调用堆栈的任何级别,因此避免无意中获得单线程上下文所有权的最可靠的方法是对每个
wait
使用
configurewait(false)
,即在整个传递闭包中

当然,有时你可能想在当前环境下继续,这很好。这就是为什么这是默认行为的表面原因。但是如果您不是真正需要它,那么我建议默认使用
ConfigureAwait(false)
。对于库代码尤其如此。库代码可以从任何地方被调用,因此最好遵循最小意外的原则。这意味着在不需要调用方上下文时,不要将其他线程锁定在调用方上下文之外。即使在库代码的任何地方使用
ConfigureAwait(false)
,调用方仍然可以选择恢复其原始上下文(如果他们需要的话)


[1] 此行为可能因框架和编译器版本而异。

调用也可能同步返回