为什么在尝试在另一个线程上运行异步方法时收到警告?

Ang*_*ker 0 c# async-await

我有一个异步方法,它调用另一个异步方法,但是,我希望它在单独的线程上并行运行:

public async Task<Page> ServePage() {
  Task.Run(() => DoThings(10));   // warning here

  // ... other code
  return new Page();
}

public async Task DoThings(int foo) {
  // stuff
}
Run Code Online (Sandbox Code Playgroud)

警告指出:

由于不等待此调用,因此在调用完成之前将继续执行当前方法。考虑将“await”运算符应用于调用结果。

事实上,这就是我正在努力做的事情。为什么我会收到编译器警告?Task.Run 的语法是否不正确?

Stu*_*tLC 6

长话短说

您收到警告的原因是

 Task.Run(() => DoThings(10));   // warning here
Run Code Online (Sandbox Code Playgroud)

返回一个任务,并且由于您的ServePage方法被标记为异步,编译器认为您应该等待该方法的结果Task

细节

您正在混合两种截然不同的范例,巧合的是它们都涉及Task,即:

  • Task.Run(),这对于通过利用可用的多个核心来并行化 CPU 密集型工作通常很有用
  • async / await,这对于等待 I/O 绑定操作完成而不阻塞(浪费)线程很有用。

例如,如果您想同时执行 3 x CPU 密集型操作,并且由于Task.Run返回 a Task,您可以执行以下操作:

public Page ServePage() // If we are CPU bound, there's no point decorating this as async
{
    var taskX = Task.Run(() => CalculateMeaningOfLife()); // Start taskX
    var taskY = Task.Run(() => CalculateJonSkeetsIQ()); // Start taskY
    var z = DoMoreHeavyLiftingOnCurrentThread();
    Task.WaitAll(taskX, taskY); // Wait for X and Y - the Task equivalent of `Thread.Join`
    
    // Return a final object comprising data from the work done on all three tasks
    return new Page(taskX.Result, taskY.Result, z);
}
Run Code Online (Sandbox Code Playgroud)

上述可能会使用最多三个线程,如果有足够的内核可以同时执行 CPU 密集型工作。但请注意,同时使用多个线程会降低系统的可伸缩性,因为在没有上下文切换的情况下可以同时提供的页面更少。

这与 不同async / await,后者通常用于在等待 I/O 绑定调用完成时释放线程。异步通常用于 Api 和 Web 应用程序中,以提高可扩展性,因为在 IO 密集型工作发生时,线程会被释放以用于其他工作。假设DoThings确实是 I/O 限制,我们可以这样做:

public async Task<string> DoThings(int foo) {
   var result = await SomeAsyncIo(foo);
   return "done!";
}
Run Code Online (Sandbox Code Playgroud)

异步工作也可以并行完成:

public async Task<Page> ServePage() {
   var task1 = DoThings(123); // Kick off Task 1
   var task2 = DoThings(234); // Kick off Task 2 in parallel with task 1
   await Task.WhenAll(task1, task2); // Wait for both tasks to finish, while releasing this thread
   return new Page(task1.Result, task2.Result); // Return a result with data from both tasks
}
Run Code Online (Sandbox Code Playgroud)

如果 I/O 绑定工作花费了合理的时间,则很有可能在await Task.WhenAll零线程实际运行期间存在一个点 - 请参阅Stephen Cleary 的文章

还有第三种但非常危险的选择,那就是即发即忘。由于方法DoThings已经被标记为async,它已经返回 a Task,所以根本不需要使用Task.Run。即发即忘看起来如下:

public Page ServePage() // No async
{
  #pragma warning disable 4014 //   warning is suppresed by the Pragma
  DoThings(10);   // Kick off DoThings but don't wait for it to complete.
  #pragma warning enable 4014

  // ... other code
  return new Page();
}
Run Code Online (Sandbox Code Playgroud)

根据 @JohnWu 的评论,“一劳永逸”的方法是危险的,并且通常表明存在设计味道。更多相关内容请点击这里这里

编辑

关于:

我一遍又一遍地忽略了其中的细微差别,例如调用从同步方法返回 Task 的异步方法会触发并忘记该方法的执行。(这是最后一个代码示例。)我理解正确吗?

解释起来有点困难,但是无论是否使用关键字调用,被调用方法中在第一个await之前的await任何同步代码都将在调用者的线程上执行,除非我们使用像.asyncTask.Run

也许这个例子可能有助于理解(请注意,我们故意使用同步Thread.Sleep,而不是await Task.Delay模拟 CPU 密集型工作并引入可以观察到的延迟)

public async Task<Page> ServePage()
{
  // Launched from this same thread, 
  // returns after ~2 seconds (i.e. hits both sleeps) 
  // continuation printed.
  await DoThings(10);

  #pragma warning disable 4014
  // Launched from this same thread, 
  // returns after ~1 second (i.e. hits first sleep only) 
  // continuation not yet printed
  DoThings(10);   

  // Task likely to be scheduled on a second thread
  // will return within few milliseconds (i.e. not blocked by any sleeps)
  Task.Run(() => DoThings(10));   

  // Task likely to be scheduled on a second thread
  // will return after 2 seconds, although caller's thread will be released during the await
  // Generally a waste of a thread unless also doing CPU bound work on current thread, or unless we want to release the calling thread.
  await Task.Run(() => DoThings());   

  // Redundant state machine, returns after 2 seconds
  // see return Task vs async return await Task /sf/ask/1336870041/
  await Task.Run(async () => await DoThings());   
}

public async Task<string> DoThings(int foo) {
   Thread.Sleep(1000); 
   var result = await SomeAsyncIo(foo);
   Trace.WriteLine("Continuation!"); 
   Thread.Sleep(1000); 
   return "done!";
}
Run Code Online (Sandbox Code Playgroud)

还有一个需要注意的要点 - 在大多数情况下,不能保证 await 之后的延续代码将在与 .await 之前的线程相同的线程上执行await。延续代码被编译器重写为Task,延续任务将被调度到线程池上。