【问题标题】:How to handle async method that has some awaits and some cpu-bound operations如何处理具有一些等待和一些 cpu 绑定操作的异步方法
【发布时间】:2020-03-27 12:58:02
【问题描述】:

我仍然无法理解异步/等待。我试图弄清楚是否需要将 CPU 密集型工作负载包装在下面的 Task.Run 中。有人能告诉我哪种方法更好吗?

没有Task.Run:

public async Task ProcessData()
{
    //Get some data
    var httpClient = new HttpClient();
    var response = await httpClient.GetAsync("myUrl");
    string data = await response.Content.ReadAsStringAsync();

    //Calculate result
    string result = null;
    for (int i = 0; i < 10000000; i++)
    {
        //Do some CPU intensive work
    }

    //Save results
    using (var fs = new FileStream("myFile.txt", FileMode.Create))
    using (var writer = new StreamWriter(fs))
    {
        await writer.WriteLineAsync(result);
    }
}

使用 Task.Run:

public async Task ProcessData()
{
    //Get some data
    var httpClient = new HttpClient();
    var response = await httpClient.GetAsync("myUrl");
    string data = await response.Content.ReadAsStringAsync();

    //Calculate result
    string result = null;
    await Task.Run(() =>
    {
        for (int i = 0; i < 10000000; i++)
        {
            //Do some CPU intensive work
        }
    });

    //Save results
    using (var fs = new FileStream("myFile.txt", FileMode.Create))
    using (var writer = new StreamWriter(fs))
    {
        await writer.WriteLineAsync(result);
    }
}

我的直觉是不使用 Task.Run,​​但在这种情况下,这是否意味着异步方法中的所有代码都将在单独的线程上执行?意味着调用线程直到最后才会被中断(文件流关闭并且方法结束)?

或者调用者线程是否会被中断并返回此方法以首先执行 CPU 密集型部分,然后在 ProcessData() 为 FileStream 部分在后台运行时返回它正在执行的任何操作?

我希望这很清楚。

提前致谢

【问题讨论】:

  • 使用 Parallel.Foreach() 进行多线程 CPU 绑定工作。 Parallel.Foreach(Enumerable.Range(0, 10000000), i =&gt; //Do some CPU intensive work);
  • 我看不出有一个任务运行 cpu 工作与你等待它之后没有它有什么区别。另一方面,如果你想加快这部分的速度,你可以并行化它,但你不会为此使用 async/await 和任务。
  • 您打算从 Windows 窗体或 WPF 应用程序的事件处理程序中调用 ProcessData 方法吗?还是会从 ASP.NET 应用程序内部调用?顺便说一句,根据guidelines,异步方法应该有后缀Async。所以ProcessData方法应该命名为ProcessDataAsync
  • 此代码只是一个示例。我不能使用 parallel.foreach 因为 CPU 绑定的工作实际上是一个外部库调用。就上下文而言,这是一个 Windows 服务应用程序。

标签: c# async-await


【解决方案1】:

正如我在async intro 中所描述的,async 方法就像任何其他方法一样开始执行,直接在调用线程的堆栈上执行。当await 异步操作时,它将捕获“上下文”,“暂停”该方法,然后返回给它的调用者。稍后,当“异步等待”(await) 完成时,该方法会在捕获的上下文中恢复。

所以,这意味着:

这是否意味着异步方法中的所有代码都将在单独的线程上执行?

There is no separate thread。根本没有线程“执行”await,因为无事可做。当await 完成时,该方法继续在该捕获的上下文中执行。有时这意味着该方法排队到消息队列中,最终将由原始调用线程运行(例如,UI 应用程序以这种方式工作)。其他时候,这意味着该方法由任何可用的线程池线程运行(大多数其他应用程序都以这种方式工作)。

意思是调用线程直到最后都不会被中断(文件流关闭并且方法结束)?或者调用者线程是否会被中断并返回此方法首先执行 CPU 密集型部分,然后在 ProcessData() 为 FileStream 部分在后台运行时返回它正在执行的任何操作?

调用线程永远不会被中断。

工作的 CPU 绑定部分将在 await 处捕获的任何“上下文”上运行。这可以是原始线程,也可以是线程池线程,具体取决于上下文。

那么,回到原来的问题:

谁能告诉我哪种方法更好?

如果这是可以从不同上下文调用的代码,那么我建议不要使用Task.Run,但我确实建议记录该方法确实可以完成 CPU 密集型工作。这样,如果调用者从 UI 线程调用此方法,那么它知道在调用它时使用Task.Run,以确保 UI 线程不会被阻塞。

【讨论】:

  • 感谢斯蒂芬的回答。我已经多次阅读您所有与异步相关的文章,但我仍然感到困惑。尤其是您的“没有线程”文章,因为我很难理解它。甚至微软自己的docs 也称它们为线程。这意味着一个任务将从线程池中获取一个线程,这意味着如果我启动一个任务,它将在不同的“线程”上执行。我知道线程会在等待时返回线程池,但我们仍然允许并发,对吗?
  • 是的,the problem is that Task is two completely different things:它是 Promise 类型(用于异步编程),也是一种增强型 Delegate 类型(用于并行编程)。您链接到的文档用于并行编程,而不是异步编程,因此他们从与您使用它的方式完全不同的角度谈论 Task 类型。
【解决方案2】:

使用async 执行CPU 密集型工作是为responsiveness reasons 提供的。

为什么异步在这里有帮助?

async 和 await 是在需要响应时管理 CPU 密集型工作的最佳实践。

此时,在工作安排好之后,您需要同时在当前线程上执行其他工作,或者您需要让线程返回以便它可以执行其他操作。

例如,通常您希望避免在 UI 线程上/在 UI 同步上下文中进行 CPU 密集型工作,因为您最终可能会遇到冻结/无响应的 UI。在这种情况下,您可以选择使用Task.Run 将工作安排到线程池中的下一个可用线程。

另外,假设您的代码已经在线程池线程上,这并不意味着您永远不应该使用Task.Run,因为您有充分的理由可以并行运行受 CPU 限制的工作。

例如,您想要进行计算,并且在等待结果时,您想要调用远程 API 来处理其他事情,这样您就可以在它们都返回时将结果组合在一起。

真的取决于你的情况。但是您可能需要注意的是,当您将async 用于 CPU 密集型工作时,上下文切换会产生少量开销成本。话虽如此,请注意代码中发生上下文切换的时间。

请务必注意,使用异步的成本很小,不建议用于紧密循环。由您决定如何围绕这一新功能编写代码。

【讨论】:

  • 在这种情况下,小成本并不重要,因为它是一个低优先级的 Windows 服务。我只想知道 CPU 绑定代码是否仍将在单独的线程池线程上运行。所以问题是“如果我们有一个异步方法,那么该异步方法中的所有代码,甚至是受 CPU 限制的部分,是否都在一个不是调用线程的单独线程上执行?”
  • @Sal“在不是调用线程的单独线程上执行”不能保证,所以答案是肯定的和否定的。这取决于运行异步方法时调用异步方法的线程是否空闲。如果线程空闲,线程可以运行异步方法,但如果有另一个线程空闲,另一个线程可以使用。如果您没有为每个线程存储共享数据,则无需担心线程。
  • @Sal 使用异步方法的好处是您不会阻塞线程。当有await 时,底层代码会检查Task 是否已经完成,如果没有,则给它一个回调说“告诉我什么时候完成”,然后返回。通过这样做,线程没有任何东西可以执行,因此线程是空闲的并且可以做其他事情。然而,该任务将由另一个线程或同一个线程执行,并调用回调告诉调用者“我完成了”。这样一个线程可以运行多个任务。
【解决方案3】:

如果您可以接受一些限制,可能会有一个没有Task.Run 的优雅解决方案。

...
//Get some data
var httpClient = new HttpClient();
var response = await httpClient.GetAsync("myUrl").ConfigureAwait(false);
string data = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

//Calculate result
...

如果之前至少有一个异步方法真正异步运行,那么您的 CPU 绑定代码将在某个线程池线程上运行。如果两者同步完成,则以下代码将在调用线程上运行。这可能是一个问题,也可能不是。 (即如果异步代码自然同步完成,那可能是因为一些错误。那么您可能根本不需要调用 CPU 密集型代码。)

第二个限制是您丢失了原始同步上下文,await ProcessData() 可能不会在调用线程上继续。

您可以阅读更多有关 ConfigureAwait here 的信息。

【讨论】:

  • 对于上下文,这是一个 Windows 服务应用程序。这是否意味着如果我不使用 ConfigureAwait(false),cpu-bound 代码将不会在另一个线程池线程上运行?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-09
  • 1970-01-01
  • 2012-02-25
  • 2013-08-28
相关资源
最近更新 更多