为什么我需要在所有传递闭包中使用ConfigureAwait(false)?
我正在学习async / await,在我读完这篇文章之后,请不要阻止异步代码
这是async / await适用于IO和CPU绑定的方法
我从@Stephen Cleary的文章中注意到了一个提示。
使用ConfigureAwait(false)来避免死锁是一种危险的做法。 在阻塞代码调用的所有方法(包括所有第三方和第二方代码)的传递闭包中,您必须对每个等待使用ConfigureAwait(false)。 使用ConfigureAwait(false)来避免死锁充其量只是一个hack)。
它再次出现在我上面附上的post的代码中。
public async Task 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 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)是没用的。 请告诉我正确的方法。 提前致谢。
据我所知,当我们使用
ConfigureAwait(false)
,其余的异步方法将在线程池中运行。
关闭,但有一个重要的警告,你错过了。 在等待使用ConfigureAwait(false)
的任务后恢复时,您将在任意线程上恢复。 记下“当你恢复时”这几个字。
让我告诉你一些事情:
public async Task GetValueAsync() { return "Cached Value"; } public async Task Example1() { await this.GetValueAsync().ConfigureAwait(false); }
考虑Example1
的await
。 虽然您正在等待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(); 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
调用堆栈的任何级别,因此避免无意中获取单线程上下文所有权的最可靠方法是在每次 await
使用ConfigureAwait(false)
,即在整个传递闭包中。
当然,有时你可能希望恢复当前的环境,这很好。 这表面上是为什么它是默认行为。 但是如果你真的不需要它,那么我建议默认使用ConfigureAwait(false)
。 对于库代码尤其如此。 可以从任何地方调用库代码,因此最好遵循最少惊喜的原则。 这意味着当您不需要时,不会将其他线程锁定在调用者的上下文之外。 即使您在库代码中的任何地方使用ConfigureAwait(false)
,您的调用者仍然可以选择在原始上下文中恢复,如果这是他们想要的。
[1]此行为可能因框架和编译器版本而异。