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

vie*_*uoc 25 c# multithreading asynchronous

我正在学习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
}
Run Code Online (Sandbox Code Playgroud)

据我所知,当我们使用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
}
Run Code Online (Sandbox Code Playgroud)

这意味着在使用块中第二次使用ConfigureAwait(false)是没用的.请告诉我正确的方法.提前致谢.

Mik*_*bel 30

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

关闭,但有一个重要的警告,你错过了.等待任务后恢复时ConfigureAwait(false),您将在任意线程上恢复.记下"当你恢复时"这几个字.

让我告诉你一些事情:

public async Task<string> GetValueAsync()
{
    return "Cached Value";
}

public async Task Example1()
{
    await this.GetValueAsync().ConfigureAwait(false);
}
Run Code Online (Sandbox Code Playgroud)

考虑awaitExample1.虽然您正在等待某个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();
}
Run Code Online (Sandbox Code Playgroud)

看看Example2.我们await 总是以异步方式运行的第一种方法.当我们达到第二个时await,我们知道我们正在线程池线程上运行,因此ConfigureAwait(false)第二次调用不需要,对吧? 错误. 尽管Async在名称中并且返回了a Task,我们的第二种方法并没有使用async和编写await.相反,它执行自己的调度并使用a TaskCompletionSource来传达结果.当你从你的身上恢复时await,你可能会[1]最终在提供结果的任何线程上运行,在这种情况下是WPF的调度程序线程.哎呦.

这里的关键点是你经常不知道"等待"方法的确切含义.随着还是没有CongifureAwait,你可能最终运行在某处的意外.这可以在任何层次发生的async调用堆栈,所以最可靠的办法,以避免无意中服用单线程上下文的所有权是使用ConfigureAwait(false)每一个 await,即,在整个传递闭包.

当然,有时你可能希望恢复当前的环境,这很好.这显然是为什么它是默认行为.但如果你真的不需要它,那么我建议ConfigureAwait(false)默认使用.对于库代码尤其如此.可以从任何地方调用库代码,因此最好遵循最少惊喜的原则.这意味着当您不需要时,不会将其他线程锁定在调用者的上下文之外.即使你用ConfigureAwait(false)你的库代码随处可见,你的来电将仍然有恢复的选项原初语境,如果这就是他们想要的东西.

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

  • 此外,调用有可能与数据同步返回(可能是缓存).在那种情况下,`ConfigureAwait`不做任何事情,后续调用必须使用它 (3认同)
  • 一个人想知道,似乎最佳实践建议是使用ConfigureAwait(false)来添加所有内容,这不是默认行为吗? (3认同)
  • 在这种情况下,`DoSomethingUnexpectedAsync()` 上的`ConfigureAwait(false)` 真的能解决问题吗?当我们遇到第二个等待时,我们肯定在一个线程池线程上,所以 SyncCtx 为空。在这种情况下,使用 `ConfigureAwait(false)` 与否没有区别,是吗? (3认同)
  • 仔细认真地阅读你的答案之后,现在我更好地理解`async/await ConfigureAwait`的行为.我只想留下这个链接,在收到你的回答后我也会仔细阅读[Async/Await - 异步编程的最佳实践](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx).在链接中有**部分图6处理在等待之前完成的返回任务**与你所说的相同.谢谢. (2认同)