【问题标题】:C# async/await chaining with ConfigureAwait(false)C# async/await 链与 ConfigureAwait(false)
【发布时间】:2015-04-30 20:01:29
【问题描述】:

根据包括this excellent one here在内的大量书籍和博客,很明显,当一个dll库公开辅助异步方法(即包装方法)时,通常认为在内部完成实际的I/O任务是一种最佳实践。像这样的线程池线程上的异步方法(为简洁起见,下面显示了伪代码,我以HttpClient 为例)

public Async Task<HttpResponseMessage> MyMethodAsync(..)
{
    ...
    var httpClient = new HttpClient(..);
    var response = await httpClient.PostAsJsonAsync(..).ConfigureAwait(false);
    ...
    return response;
}

这里的关键是ConfigureAwait(false) 的使用,这样IO 任务完成发生在线程池线程上,而不是在原始线程上下文上,从而潜在地防止死锁。

我的问题是从来电者的角度出发的。我对调用者和上述方法调用之间存在多层方法调用的场景特别感兴趣,如下例所示。

CallerA -> Method1Async -> Method2Async -> finally the above MyMethodAsync

仅在最终方法上使用ConfigureAwait(false) 是否足够,或者是否还应该确保Method1Async 和Method2Async 也在内部使用ConfigureAwait(false) 调用它们的异步方法? 将它包含在所有这些中间方法中似乎很愚蠢,特别是如果Method1Async 和Method2Async 只是最终调用MyMethodAsync 的重载。 有什么想法,请赐教!

使用示例更新 因此,如果我有一个具有以下私有异步方法的库,

private async Task<string> MyPrivateMethodAsync(MyClass myClass)
{
    ...
    return await SomeObject.ReadAsStringAsync().ConfigureAwait(false);
}

我是否应该确保以下公共重载方法都包括 ConfigureAwait(false),如下所示?

public async Task<string> MyMethodAsync(string from)
{
        return await MyPrivateMethodAsync(new (MyClass() { From = from, To = "someDefaultValue"}).ConfigureAwait(false);
}
public async Task<string> MyMethodAsync(string from, string to)
{
        return await MyPrivateMethodAsync(new (MyClass() { From = from, To = to }).ConfigureAwait(false);
}

【问题讨论】:

标签: c# .net task-parallel-library async-await io-completion-ports


【解决方案1】:

绝对不是。 ConfigureAwait 顾名思义就是配置await。它只影响与之结合的await。

ConfigureAwait 实际上返回一个不同的等待类型,ConfiguredTaskAwaitable 而不是 Task 反过来又返回一个不同的等待类型 ConfiguredTaskAwaiter 而不是 TaskAwaiter

如果您想忽略所有awaits 的SynchronizationContext,您必须为每个ConfigureAwait(false) 使用。

如果你想限制ConfigureAwait(false)的使用,你可以在最顶部使用我的NoSynchronizationContextScope(见here):

async Task CallerA()
{
    using (NoSynchronizationContextScope.Enter())
    {
        await Method1Async();
    }
}

【讨论】:

  • 我认为“必须......对于 每个 他们”有点过于严格 - 你是否连续有 2 个以上 await 并且第一个真正异步返回其余的不需要ConfigureAwait(false),因为你在调用返回时丢失了上下文......但实际上ConfigureAwait(false)应该在所有调用中(或没有)。
  • @i3arnon 一个关于您的 NoSynchronizationContextScope 的快速问题,它看起来很棒,我想了解它的用例。以我上面的例子为例,ru 说,通过一点额外的编码来包含你的 NoSynchronizationContextScope,CallerA 现在可以指示调用树上的方法,即 Method1Async -> Method2Async -> 一直到 MyMethodAsync 暂时忽略 SC,无论是否这些方法中是否有 ConfigureAwait(false)?
  • @SamDevx 差不多。它实际上比这更严厉。它会暂时删除 SC,因此内部方法没有可忽略的 SC。
  • @i3arnon,啊哈明白了,谢谢你,说得通!一个实际的使用场景是,如果库编写者和库的最终用户是两个不同的实体,他们无法访问彼此的源代码,那么至少,每个实体仍然可以继续并最终决定 SC 是否应该在他们的电话中被避免。你同意吗?您是否还认为库编写器通常应该公开带有额外参数“useSC”的异步方法,默认为“false”,并在内部使用 ConfigureAwait(F) 或不在 if then 块内?
  • @SamDevx 我认为库开发人员应该始终使用ConfigureAwait,除非有特定原因不使用(例如在 WPF 库中)。消费者应该关心自己的代码并在需要时使用ConfigureAwait,但没有理由为库做出这样的决定。
【解决方案2】:

当等待任务时,它会创建一个对应的TaskAwaiter 来跟踪同时捕获当前SynchronizationContext 的任务。任务完成后,等待者在捕获的上下文上运行等待(称为延续)之后的代码。

您可以通过调用 ConfigureAwait(false) 来防止这种情况发生,这会创建一种不同类型的 wiatable (ConfiguredTaskAwaitable) 及其相应的等待者 (ConfiguredTaskAwaitable.ConfiguredTaskAwaiter),不会在捕获的上下文上运行延续。

关键是对于每个await,都会创建一个不同的等待者实例,它不是在方法或程序中的所有等待者之间共享的东西。所以最好为每个await 语句调用ConfigureAwait(false)。

你可以看到等待者的源代码here。

【讨论】:

  • 一个小修正:当Task 创建时,它不 捕获当前的SynchronizationContext。当Task.GetAwaiter() 被调用时(由编译器为await 生成的代码),此时SynchronizationContext 被捕获。
  • @Noseratio 感谢您的评论。我从Task source 得到这个。 Task.GetAwaiter() 似乎没有这样做,尽管我认为它做到了。我一定是错过了什么
  • 我没有说TaskAwaiter 在创建 SC 时捕获它。它在其实现 ICriticalNotifyCompletion.OnCompleted/UnsafeOnCompleted here 时做到了这一点,这是任何等待者的关键方法。
  • 我现在看到了,看起来OnCompleted 在Task 完成后没有被调用:)
  • 实际上,UnsafeOnCompleted 在await 点被同步调用,以存储继续回调在任务开始其异步逻辑之前。然后,TaskAwaiter 有责任在 任务完成时调用此回调。
猜你喜欢
  • 2014-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-18
  • 1970-01-01
  • 2017-06-22
  • 2021-11-13
相关资源
最近更新 更多