3

我仍然无法解决异步/等待问题。我试图弄清楚是否需要将 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 部分在后台运行时返回它正在执行的任何操作?

我希望这很清楚。

提前致谢

4

3 回答 3

5

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

所以,这意味着:

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

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

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

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

工作的 CPU 绑定部分将在await. 这可以是原始线程,也可以是线程池线程,具体取决于上下文。

那么,回到最初的问题:

有人能告诉我哪种方法更好吗?

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

于 2020-03-27T02:43:51.643 回答
2

用于async执行 CPU 密集型工作是出于响应性原因

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

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

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

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

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

例如,你想做一个计算,在等待结果的同时,你想调用一个远程 API 来做其他事情,这样你就可以在结果都返回时将它们组合在一起。

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

需要注意的是,使用 async 的成本很小,并且不建议用于紧密循环。由您决定如何围绕这一新功能编写代码。

于 2020-03-27T00:13:51.117 回答
1

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的更多信息。

于 2020-03-27T04:58:02.793 回答