【问题标题】:Why do i need to use ConfigureAwait(false) in all of transitive closure?为什么我需要在所有传递闭包中使用 ConfigureAwait(false)?
【发布时间】:2018-02-16 01:28:11
【问题描述】:

我正在学习 async/await 并且在阅读了这篇文章后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
}

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

【问题讨论】:

    标签: c# multithreading asynchronous


    【解决方案1】:

    据我所知,当我们使用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方法也不例外。

    让我们看另一个示例,这次来自 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 always 的第一个方法异步运行。当我们点击第二个await 时,我们知道我们正在线程池线程上运行,所以第二次调用不需要ConfigureAwait(false),对吧? 错误。 尽管名称中有Async 并返回Task,但我们的第二个方法并未使用asyncawait 编写。相反,它执行自己的调度并使用TaskCompletionSource 来传达结果。当您从await 恢复时,您可能[1]最终在提供结果的任何线程上运行,在本例中是 WPF 的调度程序线程。哎呀。

    这里的关键点是您通常不知道确切“等待”方法的作用。 or 没有ConfigureAwait,你可能会跑到意想不到的地方。这可能发生在async 调用堆栈的任何级别,因此避免无意中获得单线程上下文所有权的最可靠方法是将ConfigureAwait(false)每个 await 一起使用,即,贯穿传递闭包。

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

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

    【讨论】:

    • 另外,调用也有可能与数据同步返回(可能被缓存了)。在这种情况下,ConfigureAwait 什么都不做,后续调用必须使用它
    • 在我认真认真地阅读了您的回答后,现在我对async/await ConfigureAwait的行为有了更好的理解。我只是想留下这个链接,在收到你的回答Async/Await - Best Practices in Asynchronous Programming 后我也仔细阅读了。在链接中有部分 图 6 处理在等待之前完成的返回任务,它与您所说的相同。谢谢。
    • @Roar 不,它没有。 Task.FromResult 制作的 Task&lt;&gt; 已经处于完成状态。结果已经可用,其await 将同步完成,Example1 上方的await 也将同步完成。
    • 有人想知道,当最佳实践建议似乎是用 ConfigureAwait(false) 填充所有内容时,这不是默认行为吗?
    • ConfigureAwait(false) on DoSomethingUnexpectedAsync() 在这种情况下真的能解决问题吗?当我们点击第二个 await 时,我们肯定是在线程池线程上,所以 SyncCtx 为空。在这种情况下使用ConfigureAwait(false)或不使用没有区别,是吗?
    猜你喜欢
    • 2013-02-01
    • 1970-01-01
    • 2019-02-23
    • 1970-01-01
    • 2020-10-11
    • 1970-01-01
    • 2016-05-17
    • 1970-01-01
    • 2020-07-27
    相关资源
    最近更新 更多