首页 文章

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

提问于
浏览
21

我正在学习异步/等待,在我读完这篇文章之后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)是没用的 . 请告诉我正确的方法 . 提前致谢 .

1 回答

  • 21

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

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

    让我告诉你一些事情:

    public async Task<string> 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<string>();
        Dispatcher.BeginInvoke(Action(() => tcs.SetResult("Done!")));
        return tcs.Task;
    }
    
    async Task Example2()
    {
        await DoSomethingBenignAsync().ConfigureAwait(false);
        await DoSomethingUnexpectedAsync();
    }
    

    看看 Example2 . 我们的第一个方法始终是异步运行的 . 当我们点击第二个 await 时,我们知道第二次通话时我们不需要 ConfigureAwait(false) ,对吗?错误 . 尽管名称中包含 Async 并返回 Task ,但我们的第二种方法不是使用 asyncawait 编写的 . 相反,它执行自己的调度并使用 TaskCompletionSource 来传达结果 . 当你从 await 恢复时,你可能[1]最终在提供结果的任何线程上运行,在这种情况下是WPF的调度程序线程 . 哎呦 .

    这里的关键点是你经常不确切知道'awaitable'方法的作用 . 无论有没有 CongifureAwait ,您最终都可能会遇到意想不到的错误 . 这可能发生在 async 调用堆栈的任何级别,因此避免无意中获取单线程上下文所有权的最可靠方法是在每个 await 中使用 ConfigureAwait(false) ,即在整个传递闭包中使用 ConfigureAwait(false) .

    当然,有时您可能希望在当前上下文中恢复,并且's fine. That is ostensibly why it'是默认行为 . 但是如果你真的不需要它,那么我建议默认使用 ConfigureAwait(false) . 对于库代码尤其如此 . 库代码可以从任何地方调用,所以当你不需要它时,它就是's best adhere to the principle of least surprise. That means not locking other threads out of your caller'的上下文 . 即使您在库代码中的任何地方都使用 ConfigureAwait(false) ,如果这是他们想要的,您的调用者仍然可以选择恢复其原始上下文 .

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

相关问题