如果我在没有等待的情况下调用许多异步方法,然后使用Task.WhenAll,这会是异步的吗



我有一段代码,我不太确定它是否会异步运行。下面我编了一些真实反映情况的示例脚本。请注意,GetAsync方法是正确的asyn方法,具有async/await关键字,返回类型使用相关对象的Task

public async Task<SomeResults> MyMethod() 
{
var customers = _customerApi.GetAllAsync("some_url");
var orders = _orderApi.GetAllAsync("some_url");
var products = _productApi.GetAllAsync("some_url");
await Task.WhenAll(customers, orders, products);
// some more processing and returning the results
}

问题1:即使前面没有await,上面三个API调用是否会异步运行?但是,我们在Task.WhenAll之前有await

问题2:如果从Task.WhenAll之前删除await关键字,上面的代码会异步运行吗?

我试过用谷歌搜索一下,但找不到适合这种特定情况的答案。我已经开始阅读微软.NET中的并行编程,但要完成它还有很长的路要走,所以我等不及了

问题1:即使前面没有await,上述三个API调用是否会异步运行?但是,我们在Task.WhenAll之前有await

  1. 如果这些方法实际上是异步执行的,那么是的

问题2:如果从Task.WhenAll之前删除await关键字,上面的代码会异步运行吗?

  1. 如果这些方法实际上是异步执行的,那么是的。然而,在没有await的情况下使用Task.WhenAll是毫无意义的

为什么我说"if":async关键字不会神奇地使方法异步,await运算符也不会。这些方法实际上仍然必须异步执行某些操作。他们通过返回一个不完整的Task来实现这一点。

所有async方法开始时都是同步运行的,就像任何其他方法一样。魔法发生在await。如果await被赋予一个不完整的Task,那么该方法返回其自己的不完整Task,该方法的其余部分被注册为该Task的延续。只要您一直在调用堆栈上使用await,这种情况就会一直发生。

一旦Task完成,则继续运行(await之后的其余方法)。

但在调用堆栈的顶部需要一些实际上是异步的东西。如果您有一个async方法来调用一个async方法来调用同步方法,那么实际上不会有任何东西异步运行,即使您使用await也是如此。

例如,这将完全同步运行(即线程将阻塞),因为不完整的Task永远不会返回到任何位置:

async Task Method1() {
await Method2();
}
async Task Method2() {
await Method3();
}
Task Method3() {
Thread.Sleep(2000);
return Task.CompletedTask;
}

然而,这将异步运行(即,在延迟期间,线程被释放来做其他工作):

async Task Method1() {
await Method2();
}
async Task Method2() {
await Method3();
}
async Task Method3() {
await Task.Delay(2000);
}

关键在于Task.Delay返回的内容。如果您查看该源代码,您可以看到它立即(在时间结束之前)返回一个DelayPromise(它继承自Task)。由于它在等待,因此触发Method3返回一个不完整的Task。由于Method2在等待,它会返回一个不完整的Task,等等,一直到调用堆栈的上游。

YES,对这两个问题都有很多注意事项。

wait/async只是语法上的糖,它允许您以同步的方式编写异步代码。它不会神奇地启动线程以使事情并行运行。它只允许当前正在执行的线程被释放来做其他大块的工作。把await关键字想象成一把剪刀,把当前的工作块剪成两半,这意味着当前线程可以在等待结果的同时去做另一块。

为了完成这些工作,需要某种TaskScheduler。WinForms和WPF都提供TaskScheduler,允许单个线程逐个处理块,但您也可以使用默认的调度器(通过Task.Run()),该调度器将使用线程池,这意味着许多线程将同时运行许多块。

假设您使用的是单个线程,您的示例代码将运行如下:

_customerApi.GetAllAsync()将一直运行,直到它完成或达到await。此时,它将返回到调用函数的Task,该函数被填充到customers中。

则CCD_ 44将以完全相同的方式运行。任务将分配给可能完成也可能未完成的订单。

同上_productApi.GetAllAsync()

然后线程到达await Task.WhenAll(customers, orders, products);,这意味着它可以去做其他事情,所以TaskScheduler可能会给它一些其他的工作块来做,比如继续做_customerApi.GetAllAsync()的下一位。

最终,所有的工作块都将完成,您在customersordersproducts中的三项任务也将完成。此时,调度器知道它可以运行WhenAll()之后的位

因此,您可以看到,在这种情况下,SINGLE线程已经运行了所有代码,但不一定是同步的。

您的代码是否异步运行取决于您对异步的定义。如果你仔细观察所发生的事情,就会发现只有一个线程会做这些事情。然而,只要有事情要做,这个线程就不会空闲等待

真正帮助我理解异步等待的一件事是Eric Lippert采访中厨师做早餐的比喻。在中间的某个位置搜索异步等待。

假设厨师必须做早餐。他开始烧水泡茶。他没有无所事事地等水烧开,而是把面包放进烤面包机里。他不再无所事事地等待,开始烧水煮鸡蛋。茶水一开,他就泡茶,等着烤面包或鸡蛋。

异步等待类似。每当你的线程必须等待另一个进程完成时,比如要写入的文件、要返回数据的数据库查询、要加载的互联网数据,线程就不会空闲地等待另一进程完成,但它会进入调用堆栈,查看是否有调用方没有等待,并开始执行语句,直到它看到等待为止。再次进入调用堆栈并执行,直到等待。等

因为GetAllAsync被声明为async,所以你可以确定其中有一个等待。事实上,如果你声明一个函数为async而没有等待,编译器会警告你。

您的线程将进入_customerApi.GetAllAsync("some_url");并执行语句,直到它看到等待。如果您的线程正在等待的任务没有完成,那么该线程将进入调用堆栈(您的过程)并开始执行下一条语句:_orderApi.GetAllAsync("some_url")。它执行语句,直到看到等待。您的函数再次获得控制并调用下一个方法。

这种情况一直持续到您的程序开始等待。在这种情况下,可放弃的方法Task.WhenAll(不要与不可放弃的Task.WaitAll混淆)。

即使现在,线程也不会空闲等待,它会进入调用堆栈并执行语句,直到遇到等待,再次进入调用堆栈,等等

请注意:不会启动任何新线程。当您的线程忙于执行第一个方法调用的语句时,第二个调用的语句将不会被执行,而当第二个呼叫的语句正在执行时,第一个调用的任何语句都不会被执行——即使第一个等待已经准备好。

这类似于唯一的厨师:当他把面包插入烤面包机时,他无法处理煮茶的水:只有在面包插入后,他才会开始无所事事地等待烤面包,他才能继续泡茶。

await Task.WhenAll与其他等待没有什么不同,只是当所有任务都完成时,任务就完成了。因此,只要任何任务都没有准备好,线程就不会在WhenAll之后执行语句。但是:您的线程不会空闲等待,它会进入调用堆栈并开始执行语句。

因此,尽管看起来两段代码是同时执行的,但事实并非如此。如果你真的想同时运行两段代码,你必须使用`Task.run(()=>SliceTomatoes);

只有当其他任务不是异步的,并且你的线程还有其他有意义的事情要做,比如保持UI的响应性时,雇佣一个新厨师(启动一个新线程)才有意义。通常你的厨师会自己切西红柿。让打电话的人决定他是否雇了一个新厨师来做早餐和切西红柿。

我把它简化了一点,告诉你只涉及一个线程(厨师)。事实上,它可以是在等待之后继续执行语句的任何线程。您可以通过检查线程ID在调试器中看到,通常会有不同的线程继续运行。然而,这个线程与您的原始线程具有相同的上下文,因此对您来说,它就像是同一个线程:不需要互斥,用户界面线程不需要IsInvokeRequired。有关这方面的更多信息可以在Stephen Cleary 的文章中找到

最新更新