我正在学习异步/等待,在我阅读本文Don't Block on Async Code
之后和Is async/await suitable for methods that are both IO and CPU bound
我从@Stephen Cleary的文章中注意到了一个提示。
使用ConfigureAwait(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
}
据我所知,当我们使用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
}
这意味着在使用块中第二次使用ConfigureAwait(false)是没用的。请告诉我正确的方法。 提前谢谢。
答案 0 :(得分:26)
据我所知,当我们使用
ConfigureAwait(false)
时,其余的异步方法将在线程池中运行。
关闭,但有一个重要的警告,你错过了。在等待ConfigureAwait(false)
的任务后恢复时,您将在任意线程上恢复。记下“当你恢复时”。
让我告诉你一些事情:
public async Task<string> GetValueAsync()
{
return "Cached Value";
}
public async Task Example1()
{
await this.GetValueAsync().ConfigureAwait(false);
}
考虑await
中的Example1
。虽然您正在等待async
方法,但该方法实际上并不执行任何异步工作。如果async
方法没有await
任何内容,它会同步执行,并且awaiter永远不会恢复,因为它从不首先暂停。如此示例所示,对ConfigureAwait(false)
的调用可能是多余的:它们可能根本没有效果。在此示例中,当您输入Example1
时,您所处的上下文是await
之后的上下文。
不完全符合您的预期,对吗?然而,这并非完全不同寻常。许多async
方法可能包含不需要调用者挂起的快速路径。缓存资源的可用性是一个很好的例子(谢谢,@ jakub-dąbek!),但是有很多其他原因async
方法可能会提前保释。我们经常在方法的开头检查各种条件,看看我们是否可以避免做不必要的工作,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();
}
看看Example2
。我们await
始终的第一种方法是异步运行的。当我们达到第二个await
时,我们知道我们正在线程池线程上运行,因此第二次调用时不需要ConfigureAwait(false)
,对吧? 错误。尽管名称中包含Async
并返回Task
,但我们的第二种方法并非使用async
和await
编写。相反,它执行自己的调度并使用TaskCompletionSource
来传达结果。当你从await
恢复时,你可能 [1] 最终运行在提供结果的任何线程上,在这种情况下是WPF的调度程序线程。糟糕。
这里的关键点是你经常不知道完全“等待”方法的作用。如果或没有CongifureAwait
,您最终可能会在意外的位置运行。这可能发生在async
调用堆栈的任何级别,因此避免无意中获取单线程上下文所有权的最可靠方法是使用ConfigureAwait(false)
与每个 {{ 1}},即整个传递闭包。
当然,有时您可能希望在当前环境中恢复,这很好。这显然是为什么它是默认行为。但如果你真的需要它,那么我建议默认使用await
。对于库代码尤其如此。可以从任何地方调用库代码,因此最好遵循最少惊喜的原则。这意味着当您不需要时,不会将其他线程锁定在调用者的上下文之外。即使您在库代码中的任何地方使用ConfigureAwait(false)
,您的调用者仍然可以选择在他们的原始上下文中继续,如果这是他们想要的。
[1] 此行为可能因框架和编译器版本而异。