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

nil*_*ilu 35 c# task synchronizationcontext task-parallel-library async-await

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);
}
Run Code Online (Sandbox Code Playgroud)

FlatMap:

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

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

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

一种选择是为每个场景(Map和/ MapWithContextCapture或某些东西)设置单独的方法,但感觉很难看.

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

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

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

编辑

只是为了澄清一些事情:

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

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

usr*_*usr 14

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

await Map(0, i => { myTextBox.Text = i.ToString(); return 0; }); //contrived...
Run Code Online (Sandbox Code Playgroud)

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

var result = await task.ConfigureAwait(false);
return await mapper(result);
Run Code Online (Sandbox Code Playgroud)

但不是这里:

var result = await task/*.ConfigureAwait(false)*/;
...
Run Code Online (Sandbox Code Playgroud)

更可怕的是:

var result = await task.ConfigureAwait(new Random().Next() % 2 == 0);
...
Run Code Online (Sandbox Code Playgroud)

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

var result =
  someConfigFlag ? await GetSomeValue<T>() :
  await task.ConfigureAwait(false);
Run Code Online (Sandbox Code Playgroud)

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

这是设计的一个弱点someTask.

这里最令人烦恼的问题是,在调用API时不清楚会发生什么.这是令人困惑的并导致错误.因此,最好通过始终使用来确保确定性行为await.

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

await someTask.ConfigureAwait(false);
Run Code Online (Sandbox Code Playgroud)

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

这不是一个优雅的解决方案,但我认为这是可能的选择中最不邪恶的.


或者,您可以注入一个布尔参数task.ConfigureAwait(false),指定是否流动上下文.这会使行为明确.这是合理的API设计,但它使API变得混乱.关注基本API(例如Map同步上下文问题)似乎是不合适的.


Yuv*_*kov 7

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

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

你,调用者,对上下文没兴趣,所以调用它是绝对正确的ConfigureAwait(false).这有效地实现了您的需求,它可以选择内部委托是否将同步上下文包含在Map方法的调用者中.

编辑:

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

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


i3a*_*non 7

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

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

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

如果Map实现如下:

public static TResult Map<T, TResult>(this T value, Func<T, TResult> mapping)
{
    return mapping(value);
}
Run Code Online (Sandbox Code Playgroud)

您可以随意使用或不使用它Task.ConfigureAwait:

var result = await task.ConfigureAwait(false)
var mapped = result.Map(result => Foo(result));
Run Code Online (Sandbox Code Playgroud)

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

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

  • @nilu我不是在谈论具体的例子.我在谈论关注点的分离.您的实用程序方法应该是该值上的"逻辑"方法,或者是"Task"上的"async"实用程序.将两者混合在一起会让你感到麻烦. (3认同)