我们应该在调用异步回调的库中使用 ConfigureAwait(false) 吗?

Should we use ConfigureAwait(false) in libraries that call async callbacks?

关于何时使用 ConfigureAwait(false)、何时在 C# 中使用 await/async,有很多指南。

似乎一般建议在库代码中使用 ConfigureAwait(false),因为它很少依赖于同步上下文。

但是,假设我们正在编写一些非常通用的实用程序代码,它将函数作为输入。一个简单的例子可能是以下(不完整的)功能组合器,使简单的基于任务的操作更容易:

地图:

public static async Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> mapping)
{
    return mapping(await task);
}

平面地图:

public static async Task<TResult> FlatMap<T, TResult>(this Task<T> task, Func<T, Task<TResult>> mapping)
{
    return await mapping(await task);
}

问题是,在这种情况下我们应该使用ConfigureAwait(false)吗?我不确定上下文捕获是如何工作的。关闭。

一方面,如果以功能方式使用组合器,则不需要同步上下文。另一方面,人们可能会滥用 API,并在提供的函数中执行上下文相关的操作。

一个选择是为每个场景(MapMapWithContextCapture 或其他)使用单独的方法,但感觉很难看。

另一个选项可能是将选项添加到 map/flatmap 从 ConfiguredTaskAwaitable<T>ConfiguredTaskAwaitable<T>,但是由于可等待对象不必实现接口,这会导致大量冗余代码,在我看来会更糟。

有没有一种好方法可以将责任转移给调用者,这样实现的库就不需要对所提供的映射函数中是否需要上下文做出任何假设?

或者这只是一个事实,即异步方法在没有各种假设的情况下不能很好地组合?

编辑

澄清几点:

  1. 问题确实存在。当您在效用函数中执行 "callback" 时,添加 ConfigureAwait(false) 将导致空同步。上下文。
  2. 主要问题是我们应该如何应对这种情况。我们是否应该忽略有人可能想要使用同步的事实。上下文,或者除了添加一些重载、标志等之外,是否有将责任转移给调用者的好方法?

正如一些答案所提到的,可以向该方法添加一个 bool-flag,但正如我所见,这也不太好,因为它必须一直传播到API 的(因为还有更多 "utility" 函数,具体取决于上面显示的函数)。

The question is, should we use ConfigureAwait(false) in this case?

是的,你应该。如果正在等待的内部 Task 是上下文感知的并且确实使用给定的同步上下文,即使调用它的人正在使用 ConfigureAwait(false),它仍然能够捕获它。不要忘记,当忽略上下文时,您是在更高级别的调用中这样做的,不在提供的委托中。委托在 Task 中执行,如果需要,将需要上下文感知。

作为调用者,您对上下文不感兴趣,因此使用 ConfigureAwait(false) 调用它绝对没问题。这有效地完成了您想要的,它将内部委托是否包含同步上下文的选择留给了 Map 方法的调用者。

编辑:

需要注意的重要一点是,一旦您使用 ConfigureAwait(false),之后的任何方法执行都将在任意线程池线程上进行。

@i3arnon 建议的一个好主意是接受一个可选的 bool 标志,指示是否需要上下文。虽然有点丑,但会是一个不错的解决方案。

我认为这里真正的问题在于您在对 Task 的结果进行实际操作时向 Task 添加操作。

没有真正的理由将任务的这些操作作为容器复制,而不是将它们保留在任务结果中。

这样你就不需要决定如何在实用方法中await这个任务,因为这个决定留在消费者代码中。

如果 Map 实现如下:

public static TResult Map<T, TResult>(this T value, Func<T, TResult> mapping)
{
    return mapping(value);
}

无论有无 Task.ConfigureAwait,您都可以轻松地使用它:

var result = await task.ConfigureAwait(false)
var mapped = result.Map(result => Foo(result));

Map这里只是一个例子。关键是你在这里操纵什么。如果你正在操纵任务,你不应该 await 它并将结果传递给消费者委托,你可以简单地添加一些 async 逻辑,你的调用者可以选择是否使用 Task.ConfigureAwait或不。如果您正在对结果进行操作,则无需担心任何任务。

您可以将布尔值传递给这些方法中的每一个,以表示您是否要继续捕获的上下文(或者更稳健地传递选项 enum 标志以支持其他 await配置)。但这违反了关注点分离,因为这与 Map (或其等效项)没有任何关系。

当您说 await task.ConfigureAwait(false) 时,您会转换到线程池,导致 mapping 在空上下文下变为 运行,而不是在先前的上下文下 运行ning。这可能会导致不同的行为。因此,如果来电者写道:

await Map(0, i => { myTextBox.Text = i.ToString(); return 0; }); //contrived...

然后这将在以下 Map 实现下崩溃:

var result = await task.ConfigureAwait(false);
return await mapper(result);

但这里没有:

var result = await task/*.ConfigureAwait(false)*/;
...

更丑陋:

var result = await task.ConfigureAwait(new Random().Next() % 2 == 0);
...

关于同步上下文掷硬币!这看起来很有趣,但并不像看起来那么荒谬。一个更现实的例子是:

var result =
  someConfigFlag ? await GetSomeValue<T>() :
  await task.ConfigureAwait(false);

因此,根据某些外部状态,方法 运行 的其余部分可以更改的同步上下文。

这也可能发生在非常简单的代码中,例如:

await someTask.ConfigureAwait(false);

如果 someTask 在等待时已经完成,则不会切换上下文(出于性能原因,这很好)。如果需要切换,则该方法的其余部分将在线程池上恢复。

这种非确定性是 await 设计的弱点。这是以性能为名的权衡。

这里最令人头疼的问题是调用API时不清楚会发生什么。这令人困惑并导致错误。

怎么办?

备选方案 1: 您可以争辩说,最好始终使用 task.ConfigureAwait(false).

来确保确定性行为

lambda 必须确保它 运行 在正确的上下文中:

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext;
Map(..., async x => await Task.Factory.StartNew(
        () => { /*access UI*/ },
        CancellationToken.None, TaskCreationOptions.None, uiScheduler));

最好在实用方法中隐藏其中的一些内容。

备选方案 2: 您还可以争辩说 Map 函数应该与同步上下文无关。它应该不管它。然后上下文将流入 lambda。当然,同步上下文的存在可能会改变 Map 的行为(不是在这种特殊情况下,而是在一般情况下)。所以 Map 必须被设计来处理这个问题。

备选方案 3: 您可以将布尔参数注入 Map 以指定是否流式传输上下文。这将使行为明确。这是合理的 API 设计,但它使 API 混乱。将 API 等基本 Map 与同步上下文问题联系起来似乎是不合适的。

走哪条路?我觉得要看具体情况。例如,如果 Map 是一个 UI 辅助函数,那么流动上下文是有意义的。如果它是一个库函数(例如重试助手)我不确定。我可以看到所有替代方案都有意义。一般情况下,建议在所有库代码中应用ConfigureAwait(false)。在我们调用用户回调的那些情况下,我们是否应该破例?如果我们已经离开了正确的上下文怎么办,例如:

void LibraryFunctionAsync(Func<Task> callback)
{
    await SomethingAsync().ConfigureAwait(false); //Drops the context (non-deterministically)
    await callback(); //Cannot flow context.
}

很遗憾,没有简单的答案。