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

时间:2022-09-11 11:45:07

There are lots of guidelines for when to use ConfigureAwait(false), when using await/async in C#.

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

It seems the general recommendation is to use ConfigureAwait(false) in library code, as it rarely depends on the synchronization context.

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

However, assume we are writing some very generic utility code, which takes a function as input. A simple example could be the following (incomplete) functional combinators, to make simple task-based operations easier:

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

Map:

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

FlatMap:

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

The question is, should we use ConfigureAwait(false) in this case? I am unsure how the context capture works wrt. closures.

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

On one hand, if the combinators are used in a functional way, the synchronization context should not be necessary. On the other hand, people might misuse the API, and do context dependent stuff in the provided functions.

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

One option would be to have separate methods for each scenario (Map and MapWithContextCapture or something), but it feels ugly.

一种选择是为每个场景(Map和MapWithContextCapture或其他东西)分别设置方法,但感觉很难看。

Another option might be to add the option to map/flatmap from and into a ConfiguredTaskAwaitable<T>, but as awaitables don't have to implement an interface this would result in a lot of redundant code, and in my opinion be even worse.

另一种选择可能是将map / flatmap的选项添加到ConfiguredTaskAwaitable 中,但是由于等待不必实现接口,这将导致大量冗余代码,在我看来更糟糕。

Is there a good way to switch the responsibility to the caller, such that the implemented library doesn't need to make any assumptions on whether or not the context is needed in the provided mapping-functions?

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

Or is it simply a fact, that async methods don't compose too well, without various assumptions?

或者仅仅是一个事实,异步方法组成得不是很好,没有各种假设?

EDIT

Just to clarify a few things:

只是为了澄清一些事情:

  1. The problem does exist. When you execute the "callback" inside the utility function, the addition of ConfigureAwait(false) will result in a null sync. context.
  2. 问题确实存在。当您在实用程序函数中执行“回调”时,添加ConfigureAwait(false)将导致空同步。上下文。

  3. The main question is how we should tackle the situation. Should we ignore the fact that someone might want to use the sync. context, or is there a good way to shift the responsibility out to the caller, apart from adding some overload, flag or the like?
  4. 主要问题是我们应该如何处理这种情况。我们是否应该忽略某人可能想要使用同步的事实。上下文,还是有一个很好的方法将责任转移到调用者,除了添加一些重载,标志等?

As a few answers mention, it would be possible to add a bool-flag to the method, but as I see it, this is not too pretty either, as it will have to be propagated all the way through the API's (as there are more "utility" functions, depending on the ones shown above).

正如一些答案所提到的那样,可以在方法中添加一个bool-flag,但正如我所看到的,这也不是太漂亮,因为它必须一直传播到API中(因为它有)更多“实用”功能,取决于上面显示的功能。

3 个解决方案

#1


When you say await task.ConfigureAwait(false) you transition to the thread-pool causing mapping to run under a null context as opposed to running under the previous context. That can cause different behavior. So if the caller wrote:

当你说等待task.ConfigureAwait(false)时,你转换到线程池导致映​​射在空上下文中运行而不是在前面的上下文中运行。这可能会导致不同的行为。所以如果来电者写道:

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

Then this would crash under the following Map implementation:

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

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

But not here:

但不是这里:

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

Even more hideous:

更可怕的是:

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

Flip a coin about the synchronization context! This looks funny but it is not as absurd as it seems. A more realistic example would be:

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

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

So depending on some external state the synchronization context that the rest of the method runs under can change.

因此,根据某些外部状态,该方法的其余部分运行的同步上下文可能会发生变化。

This also can happen with very simple code such as:

这也可以通过非常简单的代码实现,例如:

await someTask.ConfigureAwait(false);

If someTask is already completed at the point of awaiting it there will be no switch of context (this is good for performance reasons). If a switch is necessary then the rest of the method will resume on the thread pool.

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

This non-determinism a weakness of the design of await. It's a trade-off in the name of performance.

这种非决定论是等待设计的弱点。这是性能名称的权衡。

The most vexing issue here is that when calling the API is is not clear what happens. This is confusing and causes bugs.

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

What to do?

该怎么办?

Alternative 1: You can argue that it is best to ensure deterministic behavior by always using task.ConfigureAwait(false).

备选方案1:您可以认为最好通过始终使用task.ConfigureAwait(false)来确保确定性行为。

The lambda must make sure that it runs under the right context:

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

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

It's probably best to hide some of this in a utility method.

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

Alternative 2: You can also argue that the Map function should be agnostic to the synchronization context. It should just leave it alone. The context will then flow into the lambda. Of course, the mere presence of a synchronization context might alter the behavior of Map (not in this particular case but in general). So Map has to be designed to handle that.

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

Alternative 3: You can inject a boolean parameter into Map that specifies whether to flow the context or not. That would make the behavior explicit. This is sound API design but it clutters the API. It seems inappropriate to concern a basic API such as Map with synchronization context issues.

备选3:您可以在Map中注入一个布尔参数,指定是否流动上下文。这会使行为明确。这是合理的API设计,但它使API变得混乱。关注基本API(例如具有同步上下文问题的Map)似乎是不合适的。

Which route to take? I think it depends on the concrete case. For example, if Map is a UI helper function it makes sense to flow the context. If it is a library function (such as a retry helper) I'm not sure. I can see all alternatives make sense. Normally, it is recommended to apply ConfigureAwait(false) in all library code. Should we make an exception in those cases where we call user callbacks? What if we have already left the right context e.g.:

采取哪条路线?我认为这取决于具体案例。例如,如果Map是UI辅助函数,那么流动上下文是有意义的。如果它是库函数(例如重试助手),我不确定。我可以看到所有选择都有意义。通常,建议在所有库代码中应用ConfigureAwait(false)。在我们称之为用户回调的情况下,我们应该例外吗?如果我们已经离开了正确的背景,例如:

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

So unfortunately, there is no easy answer.

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

#2


I think the real issue here comes from the fact that you are adding operations to Task while you actually operate on the result of it.

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

There's no real reason to duplicate these operations for the task as a container instead of keeping them on the task result.

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

That way you don't need to decide how to await this task in a utility method as that decision stays in the consumer code.

这样,您就不需要决定如何在实用程序方法中等待此任务,因为该决策保留在使用者代码中。

If Map is instead implemented as follows:

如果Map实现如下:

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

You can easily use it with or without Task.ConfigureAwait accordingly:

您可以使用或不使用Task.ConfigureAwait轻松地使用它:

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

Map here is just an example. The point is what are you manipulating here. If you are manipulating the task, you shouldn't await it and pass the result to a consumer delegate, you can simply add some async logic and your caller can choose whether to use Task.ConfigureAwait or not. If you are operating on the result you don't have a task to worry about.

这里的地图只是一个例子。关键是你在这里操纵什么。如果您正在操作任务,则不应等待它并将结果传递给使用者委托,您只需添加一些异步逻辑,调用者就可以选择是否使用Task.ConfigureAwait。如果您正在对结果进行操作,则无需担心任务。

You can pass a boolean to each of these methods to signify whether you want to continue on the captured context or not (or even more robustly pass an options enum flags to support other await configurations). But that violates separation of concerns, as this doesn't have anything to do with Map (or its equivalent).

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

#3


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

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

Yes, you should. If the inner Task being awaited is context aware and does use a given synchronization context, it would still be able to capture it even if whoever is invoking it is using ConfigureAwait(false). Don't forget that when disregarding the context, you're doing so in the higher level call, not inside the provided delegate. The delegate being executed inside the Task, if needed, will need to be context aware.

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

You, the invoker, have no interest in the context, so it's absolutely fine to invoke it with ConfigureAwait(false). This effectively does what you want, it leaves the choice of whether the internal delegate will include the sync context up to the caller of your Map method.

你,调用者,对上下文没兴趣,所以使用ConfigureAwait(false)调用它是绝对正确的。这样可以有效地完成您想要的操作,它可以选择内部委托是否将同步上下文包含在Map方法的调用者中。

Edit:

The important thing to note is that once you use ConfigureAwait(false), any method execution after that would be on on an arbitrary threadpool thread.

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

A good idea suggested by @i3arnon would be to accept an optional bool flag indicating whether context is needed or not. Although a bit ugly, would be a nice work around.

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

#1


When you say await task.ConfigureAwait(false) you transition to the thread-pool causing mapping to run under a null context as opposed to running under the previous context. That can cause different behavior. So if the caller wrote:

当你说等待task.ConfigureAwait(false)时,你转换到线程池导致映​​射在空上下文中运行而不是在前面的上下文中运行。这可能会导致不同的行为。所以如果来电者写道:

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

Then this would crash under the following Map implementation:

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

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

But not here:

但不是这里:

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

Even more hideous:

更可怕的是:

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

Flip a coin about the synchronization context! This looks funny but it is not as absurd as it seems. A more realistic example would be:

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

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

So depending on some external state the synchronization context that the rest of the method runs under can change.

因此,根据某些外部状态,该方法的其余部分运行的同步上下文可能会发生变化。

This also can happen with very simple code such as:

这也可以通过非常简单的代码实现,例如:

await someTask.ConfigureAwait(false);

If someTask is already completed at the point of awaiting it there will be no switch of context (this is good for performance reasons). If a switch is necessary then the rest of the method will resume on the thread pool.

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

This non-determinism a weakness of the design of await. It's a trade-off in the name of performance.

这种非决定论是等待设计的弱点。这是性能名称的权衡。

The most vexing issue here is that when calling the API is is not clear what happens. This is confusing and causes bugs.

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

What to do?

该怎么办?

Alternative 1: You can argue that it is best to ensure deterministic behavior by always using task.ConfigureAwait(false).

备选方案1:您可以认为最好通过始终使用task.ConfigureAwait(false)来确保确定性行为。

The lambda must make sure that it runs under the right context:

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

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

It's probably best to hide some of this in a utility method.

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

Alternative 2: You can also argue that the Map function should be agnostic to the synchronization context. It should just leave it alone. The context will then flow into the lambda. Of course, the mere presence of a synchronization context might alter the behavior of Map (not in this particular case but in general). So Map has to be designed to handle that.

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

Alternative 3: You can inject a boolean parameter into Map that specifies whether to flow the context or not. That would make the behavior explicit. This is sound API design but it clutters the API. It seems inappropriate to concern a basic API such as Map with synchronization context issues.

备选3:您可以在Map中注入一个布尔参数,指定是否流动上下文。这会使行为明确。这是合理的API设计,但它使API变得混乱。关注基本API(例如具有同步上下文问题的Map)似乎是不合适的。

Which route to take? I think it depends on the concrete case. For example, if Map is a UI helper function it makes sense to flow the context. If it is a library function (such as a retry helper) I'm not sure. I can see all alternatives make sense. Normally, it is recommended to apply ConfigureAwait(false) in all library code. Should we make an exception in those cases where we call user callbacks? What if we have already left the right context e.g.:

采取哪条路线?我认为这取决于具体案例。例如,如果Map是UI辅助函数,那么流动上下文是有意义的。如果它是库函数(例如重试助手),我不确定。我可以看到所有选择都有意义。通常,建议在所有库代码中应用ConfigureAwait(false)。在我们称之为用户回调的情况下,我们应该例外吗?如果我们已经离开了正确的背景,例如:

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

So unfortunately, there is no easy answer.

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

#2


I think the real issue here comes from the fact that you are adding operations to Task while you actually operate on the result of it.

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

There's no real reason to duplicate these operations for the task as a container instead of keeping them on the task result.

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

That way you don't need to decide how to await this task in a utility method as that decision stays in the consumer code.

这样,您就不需要决定如何在实用程序方法中等待此任务,因为该决策保留在使用者代码中。

If Map is instead implemented as follows:

如果Map实现如下:

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

You can easily use it with or without Task.ConfigureAwait accordingly:

您可以使用或不使用Task.ConfigureAwait轻松地使用它:

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

Map here is just an example. The point is what are you manipulating here. If you are manipulating the task, you shouldn't await it and pass the result to a consumer delegate, you can simply add some async logic and your caller can choose whether to use Task.ConfigureAwait or not. If you are operating on the result you don't have a task to worry about.

这里的地图只是一个例子。关键是你在这里操纵什么。如果您正在操作任务,则不应等待它并将结果传递给使用者委托,您只需添加一些异步逻辑,调用者就可以选择是否使用Task.ConfigureAwait。如果您正在对结果进行操作,则无需担心任务。

You can pass a boolean to each of these methods to signify whether you want to continue on the captured context or not (or even more robustly pass an options enum flags to support other await configurations). But that violates separation of concerns, as this doesn't have anything to do with Map (or its equivalent).

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

#3


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

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

Yes, you should. If the inner Task being awaited is context aware and does use a given synchronization context, it would still be able to capture it even if whoever is invoking it is using ConfigureAwait(false). Don't forget that when disregarding the context, you're doing so in the higher level call, not inside the provided delegate. The delegate being executed inside the Task, if needed, will need to be context aware.

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

You, the invoker, have no interest in the context, so it's absolutely fine to invoke it with ConfigureAwait(false). This effectively does what you want, it leaves the choice of whether the internal delegate will include the sync context up to the caller of your Map method.

你,调用者,对上下文没兴趣,所以使用ConfigureAwait(false)调用它是绝对正确的。这样可以有效地完成您想要的操作,它可以选择内部委托是否将同步上下文包含在Map方法的调用者中。

Edit:

The important thing to note is that once you use ConfigureAwait(false), any method execution after that would be on on an arbitrary threadpool thread.

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

A good idea suggested by @i3arnon would be to accept an optional bool flag indicating whether context is needed or not. Although a bit ugly, would be a nice work around.

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