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

标签 c# async-await task-parallel-library task synchronizationcontext

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

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

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

map :

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

平面 map :

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 或其他),但感觉很难看。

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

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

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

编辑

只是澄清一些事情:

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

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

最佳答案

当您说 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:您可以将 bool 参数注入(inject) Map 以指定是否流动上下文。这将使行为明确。这是合理的 API 设计,但它使 API 变得困惑。将诸如 Map 之类的基本 API 与同步上下文问题联系起来似乎是不合适的。

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

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

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

关于c# - 我们应该在调用异步回调的库中使用 ConfigureAwait(false) 吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30036459/

相关文章:

c# - 递归实验

c# - Debug.Write 不工作

c# - 在 C# 构造函数中等待 AsyncMethod

rust - 有没有办法为 tokio::spawn_blocking 创建多个池,这样一些任务就不会饿死其他任务?

c# - 将 Task 包装为 Task<TResult> 的最佳方法是什么

c# - 并发受限的 TaskScheduler 会阻塞自身

c# - IIS 中托管的 WCF 服务的生命周期

c# - 实现 SOLR.Net 和 LUCENE.Net

c# - 如何设计流畅的异步操作?

c# - 计算进度逻辑应该驻留在服务层吗?