【问题标题】:"await task.ConfigureAwait(false)" versus "await ContextSwitcher.SwitchToThreadPool()" [closed]“await task.ConfigureAwait(false)”与“await ContextSwitcher.SwitchToThreadPool()”[关闭]
【发布时间】:2020-03-24 14:08:34
【问题描述】:

广泛推荐像这样使用ConfigureAwait(false)

await Do1Async().ConfigureAwait(false);
// ...
await Do2Async().ConfigureAwait(false);
// ...
await Do3Async().ConfigureAwait(false);
// ...

IIRC,同时广泛不鼓励使用类似ContextSwitcher 的东西,它将异步执行流上下文切换到池线程,因此可能有助于避免ConfigureAwait 在我的方法中的侵扰:

await ContextSwitcher.SwitchToThreadPool(); // this was even removed from async CTP

await Do1Async();
// ...
await Do2Async();
// ...
await Do3Async();
// ...

为什么第一个选项被认为是一个好的做法而这个不是,特别是考虑到await Do1Async().ConfigureAwait(false) 之后的代码将在与await ContextSwitcher.SwitchToThreadPool() 之后的代码完全相同的条件下继续运行?

另外,还有另一种选择

await Task.Run(async () => {
   await Do1Async();
   // ...
   await Do2Async();
   // ...
   await Do3Async();
   // ...
});

IIRC,这仍然比 ContextSwitcher 选项好,但为什么呢?

最后还是有这个有趣的方法:An alternative to ConfigureAwait(false) everywhere

这是来自the author's repoSynchronizationContextRemover的相关部分:

public void OnCompleted(Action continuation)
{
    var prevContext = SynchronizationContext.Current;
    try
    {
        SynchronizationContext.SetSynchronizationContext(null);
        continuation();
    }
    finally
    {
        SynchronizationContext.SetSynchronizationContext(prevContext);
    }
}

像这样删除同步上下文是否安全,AFAIU 会在await new SynchronizationContextRemover() 之后影响整个同步范围?

await new SynchronizationContextRemover();
// we are still on the same thread 
// but the synchronization context has been removed, 
// be careful...
// ...
await Do1Async();
// ...until now

这个SynchronizationContextRemoverContextSwitcher 好多少,而且它可能少了一个池线程的切换?

【问题讨论】:

  • “广泛鼓励”的声明可能太强了 - stackoverflow.com/questions/13489065/…... 从来没有完全鼓励过,除非在作者知道他们不需要上下文的库代码中......甚至在 Core 中不太有用...
  • 这些问题对于 ASP.NET Core 不再重要(那里没有同步上下文),但对 UI 应用程序可能仍然有效。我个人 stopped using ConfigureAwait(false) even in my libraries 并在我的前端代码中使用了 await Task.Run(async () => { ... }) 选项,我可能会从减少上下文切换的数量中受益。
  • 老实说,我不认为SwitchToThreadPool 或类似@avo 有什么问题。 .NET 有一个 open issue to add ThreadPool.Yield awaitable,由 David Fowler 本人撰写,Stephen Toub's comment。另请检查this issue。不久前我也问过similar question
  • @noseratio 谢谢,该线程中有很多很好的信息,看起来 .NET 团队将在 .NET 5.0 中添加 ThreadPool.SwitchTo() awaitable!也许你应该把它作为答案?
  • 仅供参考,Stephen Toub 刚刚发布了 ConfigureAwait FAQ,这是必读的。

标签: c# .net .net-core async-await task-parallel-library


【解决方案1】:

正如其他人所指出的,ConfigureAwait(false) 在现代代码中的必要性较低(特别是,因为 ASP.NET Core 已成为主流)。此时是否在您的库中使用它是一个判断调用;就个人而言,我仍然使用它,但我的主要异步库非常低级。

尤其是考虑到await Do1Async().ConfigureAwait(false) 之后的代码将在与await ContextSwitcher.SwitchToThreadPool() 之后的代码完全相同的条件下继续运行?

条件并不完全相同 - 如果Do1Async 同步完成,则会有所不同。

为什么第一个选项被认为是一个好的做法,而这个不是

作为explained by Stephen Toub,“切换器”方法确实允许这样的代码:

try
{
  await Do1Async(); // UI thread
  await ContextSwitcher.SwitchToThreadPool();
  await Do2Async(); // Thread pool thread
}
catch (Exception)
{
  ... // Unknown thread
}

具体来说,catchfinally 块可以在未知的线程上下文中运行,具体取决于运行时行为。可以在多个线程上运行的代码更难维护。这是它从 Async CTP 中删除的主要原因(还要记住,使用当时的语言,你不能在 catchfinally 中使用 await,所以你不能切换到所需的上下文)。

IIRC,这仍然比ContextSwitcher 选项好,但为什么呢?

我认为重要的是要注意这些都是不同的语义 - 特别是,它们都在说完全不同的东西:

  • await x.ConfigureAwait(false) 说“我不在乎我在哪个线程上恢复。它可以是同一个线程或线程池线程,无论如何。” 注意:它确实不是说“切换到线程池线程”。
  • await ContextSwitcher() 表示“切换到此上下文并继续执行”。
  • await Task.Run(...) 说“在线程池线程上运行此代码,然后恢复我。”

在所有这些中,我更喜欢第一个和第三个。我用ConfigureAwait(false) 说“这个方法不关心它恢复的线程”,我用Task.Run 说“在后台线程上运行这个代码”。我不喜欢“切换器”方法,因为我发现它使代码更难维护。

像这样删除同步上下文是否安全,AFAIU 会在await new SynchronizationContextRemover() 之后影响整个同步范围?

是的;棘手的部分是它需要影响 同步 范围。这就是为什么没有Func<Task>Func<Task<T>> overloads for my SynchronizationContextSwitcher.NoContext method,它们的作用几乎相同。 NoContextSynchronizationContextRemover 之间的一个主要区别是我的强制作用域(一个 lambda)在其中没有上下文,并且移除器采用“切换器”的形式。因此,我再次强制代码说“在没有上下文的情况下运行此代码”,而切换器说“在我的方法中的此时,删除上下文,然后继续执行”。在我看来,显式范围的代码更易于维护(同样,考虑到 catch/finally 块),这就是我使用这种 API 风格的原因。

这个SynchronizationContextRemoverContextSwitcher 好多少,而且它可能少了一个池线程的切换?

SynchronizationContextRemoverNoContext 都留在同一个线程上;他们只是暂时删除该线程上的上下文。 ContextSwitcher 确实切换到了线程池线程。

【讨论】:

  • 感谢斯蒂芬您的模范回答!在现代 C# 中,await 内的 await 似乎 .NET 团队 is bringing back ThreadPool.SwitchTo 不再是问题。你能解释一下为什么你认为这个选项不那么可维护吗?
  • 顺便说一句,该问题线程也有 Stephen Toub 的评论,他现在说:“我们删除了这些,因为您不能在 catch/finally [... ] 然而,C# 已经获得了在 catch/finally 块中等待的能力,这在很大程度上是一个没有实际意义的论点。"
  • 也许我不明白catch 内部的未知线程的意义,特别是因为ConfigureAwait 如果异步完成没有什么不同:try { await Do1Async().ConfigureAwait(false); } catch (Exception) { /* Unknown thread* /}。但我想我现在明白了:我们通常不应该在库代码中捕获异常,而库是通常使用 ConfigureAwait 的地方......我说的对吗?
  • @avo:我关于可维护性的观点来自多年的多线程经验:很久以前,我采取的立场是,每个方法(或更准确地说,每个作用域代码块)都应该有一个上下文。这条规则限制了你在阅读代码时需要记住的事情的数量。在执行过程中让方法切换上下文会增加概念上的开销。此外,其他上下文的 SwitchTo 方法鼓励较少可测试的代码(ThreadPool.SwitchTo 不是问题)。
猜你喜欢
  • 2015-04-30
  • 2013-06-04
  • 2014-06-25
  • 2019-09-16
  • 1970-01-01
  • 2018-10-18
  • 2022-11-15
  • 1970-01-01
  • 2023-04-08
相关资源
最近更新 更多