使用 Task.Run 是异步等待的缩写吗?

Is using Task.Run a short hand for async await?

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

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

我可以更改函数以使用异步等待和写入的概念,而不是上面的方法:

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

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

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

我不确定你的问题标题是否与你在 body 中提出的实际问题完全匹配,所以我将使用 body:

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

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

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

有了这个(选项 2):

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

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

这个问题变得更有趣了 - 也许这就是您的意思? - 如果你这样 re-write 选项 1(我们称之为选项 3):

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

选项2和选项3相同吗?答案还是没有。

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

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

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

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

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

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

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

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

我同意一些评论,您可能希望在 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 return 有它自己的 Task - task2,这就是 return 给 [=35 的调用者的=],允许他们 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”之前以不完整的状态得到 returned。因此可以保证输出以“A”开头,但是如果不知道 AnotherMethodAsync 正在做什么,则无法预测接下来会出现“B”还是“C”。