【问题标题】:Calling await on a task created with Task.Run()对使用 Task.Run() 创建的任务调用 await
【发布时间】:2014-10-05 03:12:12
【问题描述】:

为什么可以在 C# 中做到这一点?

var task = Task.Run (...);
await task;

Task.Run() 不应该用于 CPU 绑定代码吗?为此打电话await有意义吗?

即,在调用Task.Run 之后,我了解到该任务正在线程池的另一个线程中运行。打电话await的目的是什么?打电话给task.Wait()不是更有意义吗?

最后一个问题,我的第一印象是await 旨在专门用于async 方法。用于Task.Run()返回的任务是否常见?

编辑。这也让我想知道,为什么我们有Task.Wait () 而不是Task.Await()。我的意思是,为什么Wait() 使用方法,await 使用 keyworkd。在这两种情况下使用方法不是更一致吗?

【问题讨论】:

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


    【解决方案1】:

    使用Wait 毫无意义。如果您只想让另一个线程坐在那里什么都不做,那么启动一个新线程来工作是没有意义的。这两者中唯一明智的选择是await它。等待任务是完全明智的,因为它允许原始线程继续执行。

    await 任何类型的Task 都是明智的(在正确的上下文中),无论它来自何处。 async 等待的方法没有什么特别之处。事实上,在每个异步程序中都需要有不使用async 关键字的异步方法;如果每个await 都在等待async 方法,那么您将永远无处可去。

    【讨论】:

    • 也许我没有阅读正确的文档。但似乎总是给出文档,因为 async/await 是不可分割的一对,这是错误的。
    • @sapito 您只能在本身标记为async 的方法中使用await。您等待的可以是任何Task,无论它是否使用async 生成。
    • 谢谢,我现在明白了。我想我也可以使用异步方法返回的任务调用Task.Wait()
    • @sapito 这几乎总是错的。您几乎不应该同步等待异步操作。
    • @sapito 你永远不应该如此残忍地想要对你的用户这样做。如果您想禁用 UI 中的某些控件,请禁用这些控件;不要冻结消息循环并使整个应用程序无响应。
    【解决方案2】:

    这里有几个很好的答案,但从更哲学的角度来看......

    如果您有大量 CPU 密集型工作要做,最好的解决方案通常是任务并行库,即 Parallel 或 Parallel LINQ。

    如果您有 I/O 密集型工作要做,最好的解决方案通常是围绕自然异步实现构建的 asyncawait 代码(例如,Task.Factory.FromAsync)。

    Task.Run 是一种执行单个 段受 CPU 限制的代码并从调用线程的角度将其视为异步的方法。即,如果您想做 CPU 密集型工作,但又不想干扰 UI。

    await Task.Run 构造是连接两个世界的一种方式:让 UI 线程将 CPU 密集型工作排队并异步处理。这也是 IMO 连接异步和并行代码的最佳方式,例如,await Task.Run(() => Parallel.ForEach(...))

    为什么 Wait() 使用方法,await 使用关键字。

    await 是关键字的一个原因是他们想要启用模式匹配。任务并不是唯一的“等待”。 WinRT 有自己的“异步操作”概念,它是可等待的,Rx 可观察序列是可等待的,Task.Yield 返回一个非任务等待,这使您能够在必要时创建自己的等待(例如,if you want to avoid Task allocations in high-performance socket applications)。

    【讨论】:

      【解决方案3】:

      是的,这很常见并且推荐。 await 允许异步等待任务(或任何可等待的)。确实,它主要用于自然异步操作(例如 I/O),但它也用于使用 Task.Run 卸载要在不同线程上完成的工作并异步等待它完成。

      使用Wait 不仅会阻塞调用线程,因此首先破坏了使用Task.Run 的目的,它还可能在具有单线程同步上下文的GUI 环境中导致死锁。

      最后一个问题,我的第一印象是 await 旨在专门用于异步方法

      一个方法是否实际上用async 修饰符标记是一个实现细节,.Net 中的大多数“根”任务返回方法实际上并不是async 的(Task.Delay 就是一个很好的例子)。

      【讨论】:

      • 为什么会出现死锁? Task.Run 不应该总是在单独的线程上运行吗?我认为它适用于 CPU 密集型任务。
      • @sapito Task.Run 将使用不同的线程,但在第一个线程上调用 Wait,调用线程将阻塞该线程,直到任务完成。在会导致死锁的 GUI 环境中。
      【解决方案4】:

      仅仅调用 task.Wait() 不是更有意义吗?

      不,如果您调用 Wait,您将在那里涉及两个线程,ThreadPool 中的一个工作线程正在为您工作(假设任务受 CPU 限制),而且您的调用线程将被阻塞。

      为什么要阻塞调用线程?如果调用线程是 UI 线程,结果太糟糕了!此外,如果您立即拨打Task.Run 后接Task.Wait,您也会让情况变得更糟。这并不比同步调用委托更好。启动任务后立即调用Wait 没有任何意义。

      你几乎不应该使用Wait,总是更喜欢await并释放调用线程。

      【讨论】:

        【解决方案5】:

        对于以下情况非常常见且有用(大大简化;例如,生产代码需要异常处理):

        async void OnButtonClicked()
        {
           //... Get information from UI controls ...
           var results = await Task.Run(CpuBoundWorkThatShouldntBlockUI);
           //... Update UI based on results from work run in the background ...
        }
        

        关于您稍后编辑的评论,“等待”不是方法调用。它是编译器用来决定如何实现该方法的关键字(为了清楚起见,只允许在标记为“异步”的方法中使用)。在幕后,这涉及将方法过程重写为状态机,每次使用'await'关键字时都可以暂停该状态机,然后在它等待回调以指示它已完成时恢复。这是一个简化的描述(异常传播和其他细节使事情变得复杂),但关键是“等待”不仅仅是在任务上调用方法。

        在以前的 C# 版本中,最接近这种“async/await”魔法的构造是使用“yield return”来实现IEnumerable<T>。对于枚举和异步方法,您都需要一种暂停和恢复方法的方法。 async/await 关键字(以及相关的编译器支持)从可恢复方法的基本概念开始,然后添加一些强大的功能,例如异常的自动传播、通过同步上下文调度回调(主要用于将代码保留在 UI 线程上)并自动执行所有胶水代码以设置延续回调逻辑。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2022-01-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多