繁体   English   中英

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

[英]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);
}

考虑Example1await 尽管您正在等待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 ,但我们的第二个方法不是使用asyncawait编写的。 相反,它执行自己的调度并使用TaskCompletionSource来传达结果。 当您从await恢复时,您可能[1]最终在提供结果的任何线程上运行,在这种情况下是 WPF 的调度程序线程。 哎呀。

这里的关键外卖是,你往往不知道的“awaitable”方法做什么 无论有没有ConfigureAwait ,您最终都可能会在意想不到的地方运行。 这可能发生在async调用堆栈的任何级别,因此避免无意中获得单线程上下文所有权的最可靠方法是对每个await使用ConfigureAwait(false) ,即,在整个传递闭包中。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM