我知道这个模式的作用是什么...在单独的线程中运行长时间运行的任务。但
这绝对不是这种模式的作用。
请确保您非常清楚,等待操作并没有将操作放在新线程上。 等待安排剩余的工作作为高延迟操作的继续执行。
等待不会将同步操作变成异步并发操作。
等待使正在使用已经是异步的模型的程序员编写类似于同步工作流的逻辑。等待既不创建也不销毁异步性; 它管理现有的异步性。
启动新线程就像雇佣一个工人。 当您等待任务时,您并没有雇用工人来完成该任务。 您正在询问“此任务是否已完成?如果没有,请在完成时回调我,以便我可以继续执行依赖于该任务的工作。 同时,我要在这边做另一件事...”
如果你在做税务申报,发现需要一份来自公司的号码,但信件还未到达,那么你不会
雇佣一个工人守在邮箱旁等待。你应该记录下你在税务申报中的进度,去做其他事情,当信件到达时,你可以继续之前的工作。这就是
await。它是
异步等待结果。
这种过度使用 await / async 是 Web 开发或像 Angular 这样的项目必需品吗?
它用于管理延迟。
让每一行都变成异步的方式如何提高性能呢?
有两个方面的好处。首先,它确保应用程序在高延迟操作的环境下仍然保持响应。这种性能对于不希望应用程序卡死的用户非常重要。其次,它为开发人员提供了表达异步工作流程中数据依赖关系的工具。通过不阻塞高延迟操作,系统资源可以释放出来处理未被阻塞的操作。
对我来说,这会因为启动所有这些线程而降低性能,不是吗?
这里没有线程。并发是实现异步的一种机制,不是唯一的机制。
好的,如果我像这样编写代码: await someMethod1(); await someMethod2(); await 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);
你看到这个工作流程的不同了吗?这个工作流程是:
- 把鸡蛋放在平底锅里并设置闹钟。
- 做其他工作,直到闹钟响起。
- 将鸡蛋从平底锅中取出;把面包放进烤面包机。设置闹钟......
- 做其他工作,直到闹钟响起。
- 当闹钟响起时,组装三明治。
这个工作流程效率较低,因为我们未能捕捉到烤面包和煮鸡蛋任务的高延迟和独立性。但它肯定比在等待鸡蛋煮熟时什么都不做更有效地利用资源。
整个事情的重点是:线程开销极高,所以不要启动新线程。相反,在执行高延迟操作时,让您拥有的线程更有效地工作。等待不是关于启动新线程的;而是关于在高延迟计算的世界中在一个线程上完成更多的工作。
也许计算正在另一个线程上执行,也许它被阻塞在磁盘上,无论如何,这不重要。关键是,await用于
管理异步性,而不是
创建异步性。
“我很难理解如何在没有并行处理的情况下进行异步编程。比如,在等待鸡蛋煮熟时如何告诉程序开始制作吐司,而不至于DoEggs()运行并发,至少在内部是这样的吗?”
回到类比中。你正在制作一个鸡蛋三明治,鸡蛋和吐司正在烹饪,所以你开始阅读邮件。当鸡蛋做好了一半时,你把邮件放在一边,把鸡蛋从火上取下。然后你回到了邮件。接着吐司烤好了,你就制作三明治。最后,在三明治制作完成之后,你继续阅读邮件。
你如何在没有雇员的情况下完成所有这些工作,一个人读邮件,一个人煮鸡蛋,一个人烤面包片,一个人组装三明治?你只用一个工人完成了所有工作。
通过将任务分解成小块,记录哪些部分必须按照什么顺序完成,然后通过“协作式多任务处理”来完成这些任务。如今的孩子们拥有大型平面虚拟内存模型和多线程进程,认为这就是一直以来的情况,但我的记忆可以回溯到没有这些东西的Windows 3时代。如果你想让两件事情“并行”发生,那就要将任务分解成小部分,并轮流执行它们。整个操作系统都是基于这个概念构建的。
现在,你可能会看这个比喻并说:“好吧,但是有些工作,比如烤面包,是由机器完成的”,而且正是这种方式实现了并行性。当然,我不必雇用一个工人来烤面包,但我通过硬件实现了并行性。这才是正确的思考方式。
硬件并行和线程并行是不同的。当您向网络子系统发出异步请求以从数据库中查找记录时,
没有线程等待结果。硬件实现了远低于操作系统线程的并行性水平。
如果你想更详细地了解硬件如何与操作系统配合实现异步操作,可以阅读 Stephen Cleary 的 "
There is no thread"。
因此,当你看到 "async" 时,不要认为是 "parallel",而应该认为是将高延迟操作拆分成小块。如果有许多这样的操作,它们的各个部分彼此不依赖,则可以在一个线程上
协作交错 执行这些操作的各个部分。
正如你所想象的那样,编写能够放弃当前工作、转而做其他事情,并无缝地回到之前工作的控制流非常困难。这就是我们让编译器完成这项工作的原因!"await" 的意义在于,它让你通过将异步工作流描述为同步工作流来管理这些异步工作流。在任何可以将此任务放置一旁并稍后返回的点上,都要写入 "await"。编译器会负责将您的代码转换为许多可以在异步工作流中调度的小块。
更新:
在您的最后一个示例中,有什么区别?
eggtask = FryEggAsync();
egg = await eggtask;
toasttask = MakeToastAsync();
toast = await toasttask;
egg = await FryEggAsync();
toast = await MakeToastAsync();?
我假设它同步调用它们,但是异步执行它们?我必须承认,我从来没有在单独等待任务之前费心去等待过。
没有区别。
当调用FryEggAsync时,无论是否出现await,都会调用它。 await是一个运算符。它对从调用FryEggAsync返回的东西进行操作。就像任何其他运算符一样。
再说一次:await是一个运算符,其操作数是一个任务。毫无疑问,它是一个非常不寻常的运算符,但从语法上讲,它是一个运算符,并且像任何其他运算符一样对值进行操作。
我再说一遍:
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()
返回一个代表“M 的工作将来会完成”的
Task
。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;
因为这不是期望的工作流程。
因此,对你的问题的实际答案是:在实际需要该值的时候延迟等待是一个非常好的做法,因为它增加了安排工作的机会。但是你也可以走得太远;确保所实现的工作流程是你想要的。
Task
,甚至更简化了两个新关键字await
和async
。当事情变得简单时,人们开始滥用它们,以便更简单化,因为他们在使用它们时不想再去思考它们了。 - Logman