I get what the pattern is for... to run long running tasks in a separate thread.
这绝对不是的这种模式。
Await不会将操作放在新线程上。确保对您来说很清楚。
Await将其余工作安排为高延迟操作的继续。
Await不会将同步操作转换为异步并发操作。
Await使正在使用异步模型的程序员可以编写类似于同步工作流的逻辑。等待既不会创造也不会破坏异步。它管理现有的异步。
旋转新线程就像雇用 worker 。等待任务时,您不是在雇用 worker 来执行该任务。您问的是“此任务是否已经完成?如果还没有完成,请在完成时给我回电,以便我可以继续执行依赖于该任务的工作。与此同时,我将在这里继续进行其他工作。 。”
如果您正在纳税,并且发现自己需要工作中的电话号码,而邮件尚未送达,则无需
雇用 worker 在邮箱旁等待。您记下您的税款所在,然后去做其他事情,当邮件到达时,您从上次停止的地方领取税款。等一下它异步地等待结果。
Is this excessive use of await / async something you need for web dev or for something like Angular?
这是为了管理延迟。
How is making every single line async going to improve performance?
有两种方式。首先,通过确保应用程序在高延迟操作的世界中保持响应能力。对于不希望其应用挂起的用户而言,这种性能非常重要。其次,通过为开发人员提供用于表达异步工作流中数据依赖关系的工具。通过不阻止高延迟的操作,系统资源可以释放出来以用于未阻止的操作。
To me, it'll kill performance from spinning up all those threads, no?
没有线程。并发是一种实现异步的机制。它不是唯一的。
Ok, so if I write code like: await someMethod1(); await someMethod2(); await someMethod3(); that is magically going to make the app more responsive?
相比之下,响应速度更快?与不等待它们而调用这些方法相比?不,当然不是。比起同步等待任务完成?绝对没错。
That's what I'm not getting I guess. If you awaited on all 3 at the end, then yeah, you're running the 3 methods in parallel.
不不不。停止考虑并行性。不需要任何并行性。
这样想吧。你想做一个煎鸡蛋三明治。您有以下任务:
煎鸡蛋
吐司面包
组装三明治
三个任务。第三个任务取决于前两个任务的结果,但是前两个任务并不相互依赖。因此,这是一些工作流程:
在锅里放一个鸡蛋。鸡蛋煎炸时,凝视鸡蛋。
鸡蛋煮熟后,将一些吐司放入烤面包机中。盯着烤面包机。
吐司吃完后,将鸡蛋放到吐司上。
问题是您可能在煮鸡蛋时将烤面包片放进烤面包机中。替代工作流程:
将鸡蛋放入锅中。设置一个在鸡蛋煮完时响起的警报。
将吐司放入烤面包机中。设置一个在烤面包完成时响起的警报。
检查您的邮件。做你的税。擦亮银器。无论是什么,您都需要做。
当两个警报响起时,捕获鸡蛋和吐司,将它们放在一起,就可以得到一个三明治。
您知道为什么异步工作流程效率更高吗?等待高延迟操作完成时,您会完成很多工作。
但您没有雇用鸡蛋厨师和烤面包厨师。没有新主题!
我建议的工作流程为:
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);
您看到该工作流程有何不同吗?该工作流程是:
在锅中放一个鸡蛋并设置一个警报。
进行其他工作,直到警报响起。
把鸡蛋拿出锅;把面包放进烤面包机。设置闹钟...
进行其他工作,直到警报响起。
警报响起时,组装三明治。
此工作流程效率较低,因为我们未能捕捉到吐司和鸡蛋任务具有高延迟且独立的事实。但这肯定比等待鸡蛋煮熟时无所事事更有效地利用资源。
整个过程的重点是:线程异常昂贵,因此不要增加新线程。相反,通过在执行高延迟操作时将其投入使用,可以更有效地利用您拥有的线程。等待不是要增加新线程。它是关于在具有高延迟计算的世界中在一个线程上完成更多工作。
也许计算是在另一个线程上完成的,也许是在磁盘上阻塞了,无论如何。没关系关键是,等待是为了管理异步,而不是创建异步。
I'm having a difficult time understanding how asynchronous programming can be possible without using parallelism somewhere. Like, how do you tell the program to get started on the toast while waiting for the eggs without DoEggs() running concurrently, at least internally?
回到类比。您正在做一个鸡蛋三明治,鸡蛋和吐司在做饭,所以您开始阅读邮件。鸡蛋煮好后,您会通过邮件的一半,因此将邮件放在一边,将鸡蛋从火上移开。然后,您返回到邮件。然后烤面包,然后做三明治。然后,您将在三明治制成后阅读完邮件。
在没有雇用员工,一个人阅读邮件,一个人煮鸡蛋,一个人烤面包和一个人组装三明治的情况下,您是怎么做的? 只需一个 worker 即可完成所有工作。
你是怎么做到的?通过将任务分解成小块,注意到必须按什么顺序完成哪些部分,然后协作地对这些任务进行多任务处理。
如今,拥有大型平面虚拟内存模型和多线程进程的 child 认为这一直都是,但我的内存可以追溯到Windows 3的时代,而Windows 3却没有。如果您希望“并行”发生两件事,那就是您要做的事情:将任务分成小部分,然后轮流执行。整个操作系统都基于此概念。
现在,您可以看一下类比并说:“好,但是某些工作,例如实际上敬酒的敬酒,是由机器完成的”,这就是并行性的来源。当然,我不必雇用 worker 来烤面包,但是我在硬件上实现了并行化。这是思考它的正确方法。
硬件并行性和线程并行性是不同的。当您向网络子系统发出异步请求以从数据库中找到一条记录时,那里没有线程在等待结果。硬件达到的并行度远远低于操作系统线程的水平。
如果您想更详细地说明硬件如何与操作系统协同工作以实现异步,请阅读Stephen Cleary的“
There is no thread”。
因此,当您看到“异步”时,不要以为“并行”。想一想“高延迟操作分成小块”如果有很多这样的操作,它们的块不相互依赖,那么您可以在一个线程上协作地交织这些块的执行。
就像您想象的那样,编写控制流非常困难,您可以在其中放弃当前正在做的事情,去做其他事情,然后无缝地从上次中断的地方开始。这就是为什么我们让编译器完成这项工作! “等待”的要点是,您可以通过将它们描述为同步工作流来管理这些异步工作流。到处都可以将任务搁置一旁并稍后再返回,请写下“await”。编译器将负责将您的代码分解为许多小片段,每个小片段都可以在异步工作流中进行调度。
更新:
In your last example, what would be the difference between
eggtask = FryEggAsync();
egg = await eggtask;
toasttask = MakeToastAsync();
toast = await toasttask;
egg = await FryEggAsync();
toast = await MakeToastAsync();?
I assume it calls them synchronously but executes them asynchronously? I have to admit I've never even bothered to await the task separately before.
没有区别。
调用
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
如何做到这一点?我不知道,我不在乎。我所知道的就是我称之为它,并且我得到了一个代表 future 值(value)的物体。也许该值是在另一个线程上产生的。也许是在此线程上产生的,但将来会产生。可能是由专用硬件生产的,例如磁盘 Controller 或网卡。我不在乎我关心我找回任务。
egg = await eggtask;
现在我们执行该任务,await
询问它“您完成了吗?”如果答案是肯定的,则给egg
该任务产生的值。如果答案为否,则M()
返回表示“M的工作将在将来完成”的Task
。 M()的其余部分被签名为eggtask
的延续,因此,当eggtask
完成时,它将再次调用M()
,而不是从头开始,而是从对egg
的赋值中选取它。 M()在任何时候都是可恢复的。编译器做了必要的魔术才能做到这一点。
所以现在我们回来了。线程继续做任何事情。在某个时候鸡蛋已经准备好了,因此调用了eggtask
的延续,这导致再次调用M()
。它从中断的位置恢复:将刚产生的鸡蛋分配给egg
。现在,我们继续进行 cargo :
toasttask = MakeToastAsync();
同样,该调用返回一个任务,我们:
toast = await toasttask;
检查任务是否完成。如果是,我们分配toast
。如果否,那么我们再次从M()返回,并且toasttask
的延续是* M()的其余部分。
等等。
消除task
变量没有任何关联。分配值的存储空间;它只是没有名字。
另一个更新:
is there a case to be made to call Task-returning methods as early as possible but awaiting them as late as possible?
给出的示例如下所示:
var task = FooAsync();
DoSomethingElse();
var foo = await task;
...
为此有一些理由。但是,让我们退后一步。 await
运算符的目的是使用同步工作流的编码约定来构建异步工作流。所以要考虑的是那个工作流程是什么?工作流对一组相关任务强加排序。
查看工作流程中所需顺序的最简单方法是检查数据依赖性。您不能在烤面包机烤面包机出来之前做三明治,所以您将不得不在某个地方买到烤面包机。由于等待从完成的任务中提取了值(value),因此在创建烤面包机任务和创建三明治之间必须有一个等待。
您还可以表示对副作用的依赖性。例如,用户按下按钮,因此您要播放警笛声,然后等待三秒钟,然后打开门,然后等待三秒钟,然后关闭门:
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;
因为这不是所需的工作流程。
因此,对您的问题的实际答案是:将等待时间推迟到实际需要该值之前,这是一个很好的做法,因为这会增加有效安排工作的机会。但是你可以走得太远。确保实现的工作流是所需的工作流。
我是一名优秀的程序员,十分优秀!