【问题标题】:Using await Task.Run(() => someMethodAsync()) vs await someMethodAsync() in a UI based app在基于 UI 的应用程序中使用 await Task.Run(() => someMethodAsync()) 与 await someMethodAsync()
【发布时间】:2021-12-02 13:04:08
【问题描述】:

我通过这个例子阅读了这个article

class MyService
{
  /// <summary>
  /// This method is CPU-bound!
  /// </summary>
  public async Task<int> PredictStockMarketAsync()
  {
    // Do some I/O first.
    await Task.Delay(1000);

    // Tons of work to do in here!
    for (int i = 0; i != 10000000; ++i)
      ;

    // Possibly some more I/O here.
    await Task.Delay(1000);

    // More work.
    for (int i = 0; i != 10000000; ++i)
      ;

    return 42;
  }
}

然后描述如何根据您使用的是基于 UI 的应用程序还是 ASP.NET 应用程序来调用它:

//UI based app:
private async void MyButton_Click(object sender, EventArgs e)
{
  await Task.Run(() => myService.PredictStockMarketAsync());
}

//ASP.NET:
public class StockMarketController: Controller
{
  public async Task<ActionResult> IndexAsync()
  {
    var result = await myService.PredictStockMarketAsync();
    return View(result);
  }
}

为什么需要在基于 UI 的应用中使用Task.Run() 来执行PredictStockMarketAsync()

在基于 UI 的应用程序中使用 await myService.PredictStockMarketAsync(); 不会也不会阻塞 UI 线程吗?

【问题讨论】:

  • 因为 await 在 WinForms/WPF 中的 UI thrad 上意味着“当您需要实际运行代码时请回到我的线程”...
  • @MarcGravell 抱歉,我的问题中的代码有误,我现在已更新(仅最后两句话)。
  • FWIW,博客的作者在这里相当活跃 - 我不能召唤他们,虽然 :) 还值得注意的是,那里的文章现在已经 8 年了,也就是 await 等是新的并且处于起步阶段(C# 5 于 2012 年 8 月发布);我希望我们对 .ConfigureAwait(false) 之类的集体知识在那时还低一些
  • @MarcGravell Blazor(在某种意义上是 asp.net 核心的一部分)确实有同步上下文。

标签: c# asynchronous async-await task-parallel-library ui-thread


【解决方案1】:

默认情况下,当您await 操作不完整时,会捕获同步上下文(如果存在)。然后,当异步工作完成重新激活时,捕获的同步上下文用于编组工作。很多场景没有sync-context,但是winforms和WPF之类的UI应用do,而sync-context代表UI线程(基本上:这是为了让默认行为变成“my代码有效”而不是“在我的 async 方法中与 UI 对话时出现跨线程异常”)。本质上,这意味着当您从 UI 线程await Task.Delay(1000) 时,它会暂停 1000 毫秒(释放 UI 线程),然后在 UI 线程上恢复。这意味着您“在这里要做的大量工作”发生在 UI 线程上,并阻塞了它。

通常,解决此问题的方法很简单:添加 .ConfigureAwait(false),它会禁用同步上下文捕获:

await Task.Delay(1000).ConfigureAwait(false);

(通常:在该方法中的 all await 之后添加这个)

请注意,只有当您知道自己不需要同步上下文提供的功能时,才应该这样做。特别是:您不会在之后尝试触摸 UI。如果您确实需要与 UI 对话,则必须明确使用 UI 调度程序。

当您使用Task.Run(...) 时,您通过不同 路由转义了同步上下文 - 即未在 UI 线程上启动(同步上下文通过活动线程指定)。

【讨论】:

  • 我认为ConfigureAwait(false),当用作从当前上下文中卸载工作的机制时,是一种黑客行为。它不能保证工作将被实际卸载,并且无法控制工作将运行的上下文。将工作卸载到ThreadPool 的正确方法是Task.Run 恕我直言。
  • @TheodorZoulias 笼统地说:await 本身没有提供关于工作地点的合同;同步上下文,当存在时(我认为同步上下文是不寻常的情况,这些天 - 尽管这取决于你所做的工作),添加该合同,并使用ConfigureAwait(false)我们'只是简单地选择抑制它,并在 等待的东西 暴露给我们的任何模型上运行。但在几乎所有情况下,“让等待的东西决定激活模型”就可以了(实际上,它几乎总是意味着“在线程池上”)。
  • @TheodorZoulias 一切都是上下文相关的;这取决于您当前所在的位置(您在哪个线程上),这是高容量还是低容量(对于任何低容量:嗯,不用担心 - 只需使用有效的方法),以及可等待操作是否是 always true async、always true sync 或介于两者之间的;作为一般规则,我尽量减少 不必要的 线程切换(实际上,这通常是await 的设计标准)和Task.Run 强制和必要线程切换
  • @DavidKlempfner 不,基本上;你会使用await Task.Run(...),否则你已经在当前请求中引入了并发,这导致问题 - 所以对于当前请求,你所做的只是添加一个额外的线程切换(高架);需要进行完全相同的工作 - 从一个池线程移动到另一个池线程并没有获得任何处理能力(哎呀,您甚至可以通过迂回路线回到同一个池线程)
  • @DavidKlempfner async/await 的主要收获是在发生外部工作/IO 时释放活动线程,我们已经这样做了;添加额外的线程切换没有任何用处,但有成本。在 UI 场景中,我们有一个额外的相关目标:离开 UI 线程 - 这证明了线程切换的成本是合理的;但是,这根本不适用于这里 - 网络场景中没有特殊的线程
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-02
  • 2020-07-16
  • 1970-01-01
  • 2016-01-29
  • 1970-01-01
相关资源
最近更新 更多