C# async/await 与 ConfigureAwait(false) 链接

C# async/await chaining with ConfigureAwait(false)

根据包括 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) 是否足够,还是应该确保 Method1AsyncMethod2Async 也在内部使用 ConfigureAwait(false) 调用它们的异步方法? 将它包含在所有这些中间方法中似乎很愚蠢,特别是如果 Method1AsyncMethod2Async 只是最终调用 MyMethodAsync 的重载。 有什么想法,请赐教!

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

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

我是否应该确保以下 public 重载方法也都包含 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);
}

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

ConfigureAwait 实际上是 returns 一个不同的等待类型,ConfiguredTaskAwaitable 而不是 Task 而 returns 一个不同的等待类型 ConfiguredTaskAwaiter而不是 TaskAwaiter

如果您想忽略所有 awaitSynchronizationContext,您必须对每个 ConfigureAwait(false) 使用

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

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

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

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

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

您可以查看服务员的源代码here