[英]Why is writing ConfigureAwait(false) on every line with await always recommended and do I really need it?
[英]Why do i need to use ConfigureAwait(false) in all of transitive closure?
我正在学习 async/await 并且在我阅读这篇文章后不要阻止异步代码
这是 async/await 适用于同时受 IO 和 CPU 限制的方法
我注意到@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
}
这意味着在 using 块中第二次使用 ConfigureAwait(false) 是无用的。 请告诉我正确的方法。 提前致谢。
据我所知,当我们使用
ConfigureAwait(false)
,异步方法的其余部分将在线程池中运行。
关闭,但您缺少一个重要的警告。 当您在使用ConfigureAwait(false)
等待任务后恢复时,您将在任意线程上恢复。 注意“当你恢复时”这几个字。
让我给你看一些东西:
public async Task<string> GetValueAsync()
{
return "Cached Value";
}
public async Task Example1()
{
await this.GetValueAsync().ConfigureAwait(false);
}
考虑Example1
的await
。 尽管您正在等待async
方法,但该方法实际上并不执行任何异步工作。 如果async
方法不await
任何东西,它会同步执行,并且等待器永远不会恢复,因为它从来没有暂停过。 如本例所示,对ConfigureAwait(false)
调用可能是多余的:它们可能根本没有效果。 在这个例子中,当你进入Example1
时你所处的任何上下文都是你在await
之后所在的上下文。
不完全是你的预期,对吧? 然而,这并非完全不寻常。 许多async
方法可能包含不需要调用者挂起的快速路径。 缓存资源的可用性就是一个很好的例子(谢谢,@jakub-dąbek!),但是async
方法可能会提前退出还有很多其他原因。 我们经常在一个方法的开头检查各种条件,看看是否可以避免做不必要的工作, 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 的调度程序线程。 哎呀。
这里的关键外卖是,你往往不知道的“awaitable”方法做什么。 无论有没有ConfigureAwait
,您最终都可能会在意想不到的地方运行。 这可能发生在async
调用堆栈的任何级别,因此避免无意中获得单线程上下文所有权的最可靠方法是对每个await
使用ConfigureAwait(false)
,即,在整个传递闭包中。
当然,有时您可能想继续当前的上下文,这很好。 这表面上就是为什么它是默认行为。 但是如果你不是真的需要它,那么我建议默认使用ConfigureAwait(false)
。 对于库代码尤其如此。 库代码可以从任何地方调用,所以最好遵循最少意外的原则。 这意味着在您不需要时不要将其他线程锁定在调用者的上下文之外。 即使您在库代码中的任何地方都使用ConfigureAwait(false)
,如果这是他们想要的,您的调用者仍然可以选择在其原始上下文中恢复。
[1]此行为可能因框架和编译器版本而异。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.