【问题标题】:Is using Task.Run a short hand for async await? [duplicate]正在使用 Task.Run 进行异步等待吗? [复制]
【发布时间】:2021-07-23 12:45:45
【问题描述】:
Task.Run(fn1sync());
Task.Run(fn2sync());

以上代码在新线程中启动每个任务,并在将控制权返回给主线程的同时并行运行它们。

我可以更改函数以使用异步等待的概念并编写:

var v1 = fn1Async()
var v2 = fn2Async()
...do some work..
Task.WaitAll(v1, v2)

现在 .NET 将自动处理是使用单线程还是多线程,并且并行运行它们并立即将控制权传递给主线程。

我什么时候需要使用这些方法中的任何一种?

【问题讨论】:

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


【解决方案1】:

我不确定您的问题标题是否完全符合您在正文中提出的实际问题,所以我将使用正文:

您的两个示例之间至少有一个非常大的区别。有了这个(选项1):

Task.Run(fn1sync());
Task.Run(fn2sync());

您无法等待这两个任务完成。它们将并行运行(可能 - 但不能保证 - 在不同的线程中)。它们都将立即开始并在完成时结束。之后的任何代码也将立即执行,无需等待任务完成。

有了这个(选项 2):

var v1 = fn1Async()
var v2 = fn2Async()
...do some work..
Task.WaitAll(v1, v2)

您正在坚持任务,然后在“做一些工作”之后,您明确地等待两者都完成,然后再继续。最后一行之后的任何内容都不会执行,直到两者都完成。

这个问题变得更有趣了——也许这就是你的意思? - 如果你以这种方式重写选项 1(我们称之为选项 3):

var v1 = Task.Run(fn1sync());
var v2 = Task.Run(fn2sync());
...do some work...
Task.WaitAll(v1, v2);

选项 2 和选项 3 是否相同?答案仍然是否定的。

当您调用异步方法/委托/等时。不等待它,这是选项 2 所做的,委托是否在调用分支可以继续之前实际完成取决于委托是否调用任何真正的异步代码。例如你可以写这个方法:

public Task fn1sync()
{
    Console.Write("Task complete!");
    return Task.CompletedTask;
}

Console.Write 行将始终在“做一些工作”之前执行。为什么?因为如果您从字面上考虑返回对象Task,那么根据定义,fn1sync 在写入控制台之前不能将Task 返回到您的调用代码(这是一个同步操作)。将“Console.Write”替换为非常慢甚至阻塞的东西,你就失去了异步的好处。

另一方面,当您使用Task.Run 时,您保证控制流将立即返回到您的下一行代码,无论委托执行多长时间,也不管委托是否实际包含任何异步代码.因此,您可以确保您的调用线程不会因委托中的任何内容而减慢速度。事实上,当您必须调用同步代码时使用 Task.Run 是个好主意,而您有理由相信这些代码会很慢 - 遗留 I/O、数据库、本机代码等。

调用async 方法而不等待它或将其包装在Task.Run(选项2)中的用例可能不太明显,但它们确实存在。只要您知道您正在调用的代码是表现良好的异步代码(即它不会阻塞您的线程),这是一种“即发即弃”的方法。例如,如果选项 2 中的 fn1Async 看起来像这样:

public Task fn1sync()
{
    return Task.Run(()=>
    {
         Console.Write("Task complete!");
    });
}

那么选项 2 和 3 将完全等效且同样有效。

更新:回答你的问题,“你能告诉我为什么主线程中的 await 会导致它阻塞等待结果,而函数内部的 await 会导致控制权传递给主线程吗?”

我同意一些 cmets,您可能希望找到关于 async/await/Task 的更全面的入门知识。但话虽如此,这都是关于分支以及 await 关键字在编译时如何解释的。

当你写作时:

async Task MyMethodAsync()
{
    Console.Write("A");
    int result = await AnotherMethodAsync();
    Console.Write("B");
    return;
}

这基本上被转换为:

Task MyMethodAsync()
{
    Console.Write("A");
    Task<int> task1 = AnotherMethodAsync();  // *should* return immediately if well behaved
    Task task2 = task1.ContinueWith((Task<int> t) => 
    {
        // t is same as task1 (unsure if they're identical instances but definitely equivalent)
        int result = t.Value;
        Console.Write("B");     ​
        ​return;
   ​ }); // returns immediately
    return task2; 
}

所有之前方法中的第一个awaited 语句同步执行 - 在调用者的线程上。但是第一个等待的语句之后的所有内容都变成了一个回调,只有在第一个等待的语句 - task1 - 完成后才会执行。

重要的是,ContinueWith 返回自己的Task - task2,这就是返回给MyMethodAsync 的调用者的内容,允许他们选择await 或不选择。保证"Console.Write("B") 行在task1 完成之前不会被执行。但是,当它相对于调用MyMethodAsync 之后的代码执行时 完全取决于那个电话是否是awaited。

因此:

async Task Main()
{
     await MyMethodAsync();
     Console.Write("C");
}

变成:

Task Main()
{
     return MyMethodAsync().ContinueWith(t => 
     {
          // Doesn't get executed until MyMethodAsync is done
          Console.Write("C");
     });
}

因此保证输出为:ABC

但是:

void Main()
{
     Task t = MyMethodAsync(); // Returns after "A" is written

     // Gets executed as soon as MyMethodAsync
     // branches to `await`ed code, returning the `Task`
     // May or may not be before "B"
     Console.Write("C");
}

按字面意思执行,t 在“A”之后但在“C”之前以不完整状态返回。因此保证输出以“A”开头,但如果不知道AnotherMethodAsync 在做什么,则无法预测接下来是“B”还是“C”。

【讨论】:

  • 你能告诉我为什么主线程中的 await 会导致它阻塞等待结果,而函数内部的 await 会导致控制权传递给主线程吗?
  • @JonasH 我同意彼得摩尔的观点。 Here 是我支持在 GUI 应用程序的异步事件处理程序中使用 Task.Run 的论点,用于包装手动编码的异步方法。论点基本上是,如果没有Task.Run,您可能会阻塞 UI 线程,因为被宣传为异步的内容实际上可能在不确定的情况下具有同步行为。
  • @variable 在正文中回答了您的后续问题。为篇幅道歉。也同意其他cmets。如果可用,现代异步 API 总是更可取!
  • 这是async Task Main,而不是async void Main。你不能等待 async void 方法
  • @variable 如果你调用 fn1async 裸而不将其包装在 Task.Run 中,那么是的。Console.Write 行保证在 fn2async 之前执行。如果您将 fn1async 调用包装在 Task.Run 中,则顺序不可预测。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多