35

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 from 和 into a ConfiguredTaskAwaitable<T>,但由于 awaitables 不必实现接口,这将导致大量冗余代码,在我看来更糟。

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

或者仅仅是一个事实,即异步方法在没有各种假设的情况下组合得不太好?

编辑

只是为了澄清一些事情:

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

正如一些答案所提到的,可以在该方法中添加一个布尔标志,但正如我所见,这也不是很漂亮,因为它必须通过 API 一路传播(因为有更多“实用”功能,取决于上面显示的功能)。

4

3 回答 3

14

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

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.
}

所以不幸的是,没有简单的答案。

于 2015-05-04T18:30:37.280 回答
8

问题是,在这种情况下我们应该使用 ConfigureAwait(false) 吗?

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

你,调用者,对上下文没有兴趣,所以用ConfigureAwait(false). 这有效地满足了您的需求,它将内部委托是否包含同步上下文的选择权留给您的Map方法的调用者。

编辑:

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

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

于 2015-05-04T17:59:11.783 回答
6

我认为这里真正的问题来自这样一个事实,即您Task在实际操作结果的同时添加操作。

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

这样,您就无需决定如何await在实用程序方法中执行此任务,因为该决定保留在使用者代码中。

IfMap实现如下:

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但这违反了关注点分离,因为这与(或其等价物)没有任何关系。

于 2015-05-04T17:59:34.153 回答