我知道该模式的用途...在单独的线程中运行长时间运行的任务。
这绝对不是这个模式的用途。
Await 不将操作放在新线程上。确保您非常清楚。 Await 将剩余工作安排为高延迟操作的延续。
Await 不会将同步操作变成异步并发操作。 Await 使正在使用已经异步的模型的程序员能够编写类似于同步工作流的逻辑。 Await 既不创建也不破坏异步;它管理现有的异步。
启动一个新线程就像雇用一个工人。当您等待一项任务时,您并不是在雇用工人来完成该任务。你在问“这个任务已经完成了吗?如果没有,完成后给我回电话,这样我就可以继续做依赖于该任务的工作。与此同时,我要去这里做另一件事.. 。”
如果您在报税时发现您需要工作中的号码,而邮件尚未到达,则您不要雇用工作人员在邮箱旁等待。你记下你的税收,去完成其他事情,当邮件来的时候,你从你离开的地方继续。那是等待。它异步等待结果。
对于 Web 开发或 Angular 之类的东西,您需要过度使用 await / async 吗?
这是为了管理延迟。
如何让每一行都异步来提高性能?
有两种方式。首先,通过确保应用程序在具有高延迟操作的世界中保持响应。这种性能对于不希望他们的应用程序挂起的用户来说很重要。其次,通过为开发人员提供在异步工作流中表达数据依赖关系的工具。通过不阻塞高延迟操作,系统资源被释放以用于未阻塞操作。
对我来说,启动所有这些线程会降低性能,不是吗?
没有线程。并发是一种实现异步的机制;它不是唯一的。
好的,如果我编写如下代码:await someMethod1();等待一些方法2();等待 someMethod3();这会神奇地使应用程序更具响应性吗?
与什么相比反应更快?与在不等待它们的情况下调用这些方法相比?不,当然不是。与同步等待任务完成相比?绝对是的。
我猜这就是我没有得到的。如果您最后等待了所有 3 个方法,那么是的,您正在并行运行这 3 个方法。
不不不。不要再考虑并行性了。不需要任何并行性。
这样想。你想做一个煎蛋三明治。你有以下任务:
三个任务。第三个任务依赖于前两个的结果,但前两个任务不相互依赖。所以,这里有一些工作流程:
- 在锅里放一个鸡蛋。煎鸡蛋时,盯着鸡蛋看。
- 鸡蛋做好后,在烤面包机里放一些吐司。盯着烤面包机看。
- 烤好后,将鸡蛋放在烤面包上。
问题是您可能在煮鸡蛋的同时将吐司放入烤面包机中。替代工作流程:
- 在锅里放一个鸡蛋。设置一个在鸡蛋完成时响起的闹钟。
- 将吐司放入烤面包机。设置一个在敬酒完成时响起的闹钟。
- 检查您的邮件。做你的税。擦亮银器。无论您需要做什么。
- 当两个警报都响起时,拿起鸡蛋和吐司,把它们放在一起,你就有了一个三明治。
您知道为什么异步工作流程效率更高吗?在等待高延迟操作完成时,您会完成很多工作。 但您没有聘请鸡蛋厨师和吐司厨师。没有新话题!
我建议的工作流程是:
eggtask = FryEggAsync();
toasttask = MakeToastAsync();
egg = await eggtask;
toast = await toasttask;
return MakeSandwich(egg, toast);
现在,比较一下:
eggtask = FryEggAsync();
egg = await eggtask;
toasttask = MakeToastAsync();
toast = await toasttask;
return MakeSandwich(egg, toast);
您知道该工作流程有何不同吗?这个工作流程是:
- 在锅里放一个鸡蛋,然后设置闹钟。
- 在闹钟响起之前去做其他工作。
- 把鸡蛋从锅里拿出来;把面包放在烤面包机里。设置闹钟...
- 在闹钟响之前继续做其他工作。
- 当闹钟响起时,组装三明治。
此工作流程效率较低因为我们未能捕捉到 toast 和 egg 任务具有高延迟且独立的事实。但它肯定比在等待鸡蛋煮熟时无所事事更有效地利用资源。
整个事情的重点是:线程非常昂贵,所以不要启动新线程。相反,通过在执行高延迟操作时将其投入工作来更有效地利用线程。 Await 不是关于启动新线程的;它是关于在具有高延迟计算的世界中在一个线程上完成更多工作。
也许计算是在另一个线程上完成的,也许它在磁盘上被阻塞了,无论如何。没关系。关键是,等待是为了管理异步,而不是创建它。
我很难理解如何在不使用并行性的情况下实现异步编程。比如,在没有 DoEggs() 至少在内部同时运行的情况下,你如何告诉程序在等待鸡蛋的同时开始吐司?
回到类比。您正在制作鸡蛋三明治,鸡蛋和吐司正在烹饪,因此您开始阅读邮件。鸡蛋煮好后,你的邮件已经完成了一半,所以你把邮件放在一边,把鸡蛋从火上拿下来。然后你回到邮件。然后吐司完成,你做三明治。然后你在三明治做好后读完你的邮件。 您是如何做到这一切的,而无需雇用员工,一个人看邮件,一个人煮鸡蛋,一个人做吐司,一个人组装三明治?你一个人就做到了工人。
你是怎么做到的?通过将任务分解成小块,注意哪些部分必须按什么顺序完成,然后合作处理多个任务这些部分。
如今拥有大型扁平虚拟内存模型和多线程进程的孩子们认为这就是它一直以来的样子,但我的记忆可以追溯到 Windows 3 的日子,而 Windows 3 没有这些。如果您希望两件事“并行”发生,那就是您所做的:将任务分成小部分并轮流执行部分。整个操作系统就是基于这个概念。
现在,您可能会看到这个类比并说“好吧,但是有些工作,例如烤吐司,是由一台机器完成的”,而 那 是并行性的来源.当然,我不必雇用工人来烤面包,但我在硬件上实现了并行性。这是正确的思考方式。 硬件并行和线程并行是不同的。当您向网络子系统发出异步请求以从数据库中查找记录时,没有线程 坐在那里等待结果。硬件在远远低于操作系统线程的水平上实现了并行性。
如果您想更详细地了解硬件如何与操作系统一起工作以实现异步,请阅读 Stephen Cleary 的“There is no thread”。
因此,当您看到“异步”时,不要认为“并行”。考虑“将高延迟操作拆分成小块”一个线程。
正如您可能想象的那样,编写控制流非常困难,您可以在其中放弃当前正在做的事情,去做其他事情,然后无缝地从中断的地方继续。这就是我们让编译器完成这项工作的原因! “等待”的意义在于,它允许您通过将这些异步工作流描述为同步工作流来管理它们。在任何地方,你都可以把这个任务放在一边,稍后再回来,写“await”。编译器会负责将您的代码转换成许多可以在异步工作流中调度的小块。
更新:
在你的最后一个例子中,有什么区别
eggtask = FryEggAsync();
egg = await eggtask;
toasttask = MakeToastAsync();
toast = await toasttask;
egg = await FryEggAsync();
toast = await MakeToastAsync();?
我假设它同步调用它们但异步执行它们?我不得不承认,我以前从来没有费心单独等待任务。
没有区别。
当FryEggAsync 被调用时,无论await 是否出现在它之前,它都会被调用。 await 是操作员。它对调用FryEggAsync 后返回的事物进行操作。它就像任何其他运算符一样。
让我再说一遍:await 是一个运算符,它的操作数是一个任务。可以肯定的是,它是一个非常不寻常的运算符,但从语法上讲,它是一个运算符,并且它像任何其他运算符一样对 value 进行操作。
让我再说一遍:await 不是你放在呼叫站点上的魔法尘土,突然那个呼叫站点被远程到另一个线程。当调用发生时调用发生,调用返回一个值,并且该值是对一个对象的引用,该对象是await 运算符的合法操作数。
是的,
var x = Foo();
var y = await x;
和
var y = await Foo();
都是一样的东西,一样的
var x = Foo();
var y = 1 + x;
和
var y = 1 + Foo();
都是一样的。
所以让我们再看一遍,因为你似乎相信await 导致异步的神话。它没有。
async Task M() {
var eggtask = FryEggAsync();
假设M() 被调用。 FryEggAsync 被调用。同步。没有异步调用之类的东西;你看到一个调用,控制权传递给被调用者,直到被调用者返回。被调用者返回一个任务,该任务表示将来可以使用的鸡蛋。
FryEggAsync 是如何做到这一点的?我不知道,我不在乎。我所知道的就是我调用它,然后我得到一个代表未来值的对象。也许该值是在不同的线程上产生的。也许它是在这个线程上产生的,但在未来。也许它是由特殊用途的硬件产生的,比如磁盘控制器或网卡。我不在乎。我关心我能拿回一个任务。
egg = await eggtask;
现在我们接手这个任务,await 会问它“你完成了吗?”如果答案是肯定的,那么egg 将被赋予任务产生的值。如果答案是否定的,则M() 返回一个Task,表示“M 的工作将在未来完成”。 M() 的剩余部分被注册为eggtask 的延续,所以当eggtask 完成时,它会再次调用M() 并把它捡起来不是从头开始,而是从分配给egg。 M() 是一种可随时恢复的方法。编译器做了必要的魔法来实现这一点。
所以现在我们回来了。线程继续做它做的任何事情。在某个时刻,鸡蛋准备好了,所以继续调用eggtask,这导致再次调用M()。它从中断的地方继续:将刚刚生产的鸡蛋分配给egg。现在我们继续卡车运输:
toasttask = MakeToastAsync();
再次,调用返回一个任务,我们:
toast = await toasttask;
检查任务是否完成。如果是,我们分配toast。如果否,则我们再次从 M() 返回,toasttask 的 继续 是 *M() 的余数。
等等。
消除task 变量没有任何关系。分配了值的存储空间;它只是没有名字。
另一个更新:
是否有一种情况可以尽早调用返回任务的方法但尽可能晚地等待它们?
给出的例子是这样的:
var task = FooAsync();
DoSomethingElse();
var foo = await task;
...
有一些案例可以解决。但是,让我们退后一步。 await 运算符的目的是使用同步工作流的编码约定构建异步工作流。所以要考虑的是工作流程是什么? 工作流 对一组相关任务进行排序。
查看工作流中所需排序的最简单方法是检查数据依赖性。在吐司从烤面包机中出来之前,您无法制作三明治,因此您将不得不在某处获得吐司。由于 await 从已完成的任务中提取值,因此在创建烤面包机任务和创建三明治之间必须有一个 await 某处。
您还可以表示对副作用的依赖。比如用户按下按钮,所以要播放警笛声,然后等三秒,然后开门,再等三秒,然后关门:
DisableButton();
PlaySiren();
await Task.Delay(3000);
OpenDoor();
await Task.Delay(3000);
CloseDoor();
EnableButton();
这样说是没有意义的
DisableButton();
PlaySiren();
var delay1 = Task.Delay(3000);
OpenDoor();
var delay2 = Task.Delay(3000);
CloseDoor();
EnableButton();
await delay1;
await delay2;
因为这不是所需的工作流程。
因此,您的问题的实际答案是:将 await 推迟到实际需要该值的时间点是一个非常好的做法,因为它增加了有效安排工作的机会。但是你可能走得太远了;确保实现的工作流程是您想要的工作流程。