为什么 Web 应用程序现在对 await/async 变得疯狂? [英] Why are web apps going crazy with await / async nowadays?

查看:24
本文介绍了为什么 Web 应用程序现在对 await/async 变得疯狂?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我来自后端/胖客户端背景,所以也许我遗漏了一些东西……但我最近查看了开源 JWT 令牌服务器的源代码,作者对 await/async 感到很疯狂.就像每个方法和每一行一样.

我明白了模式的用途……在单独的线程中运行长时间运行的任务.在我的胖客户端时代,如果一个方法可能需要几秒钟,我会使用它,以免阻塞 GUI 线程......但绝对不会在需要几毫秒的方法上使用.

过度使用 await/async 是否是 Web 开发或 Angular 之类的东西所需要的?这是在 JWT 令牌服务器中,所以甚至没有看到它与任何这些有什么关系.这只是一个 REST 端点.

如何让每一行异步来提高性能?对我来说,它会因为启动所有这些线程而降低性能,不是吗?

解决方案

我明白了模式的用途……在单独的线程中运行长时间运行的任务.

这绝对不是这个模式的用途.

Await 不会把操作放到一个新线程上.确保您非常清楚这一点.Await 将剩余工作安排为高延迟操作的继续.

Await 不会将同步操作变成异步并发操作.Await 使使用已经是异步的模型的程序员能够编写类似于同步工作流的逻辑.Await 既不创建也不破坏异步;它管理现有的异步.

创建新线程就像雇用工人一样.当您等待任务时,您并不是在雇用工人来完成该任务.你在问这个任务已经完成了吗?如果没有,完成后给我回电,这样我就可以继续做依赖于那个任务的工作.与此同时,我要去这里做另一件事..."

如果您正在报税并且发现您需要一个工作号码,而邮件还没有到达,您就不必雇用工人在邮箱旁等待.你记下你的税收在哪里,去做其他事情,当邮件来的时候,你从上次停下的地方开始.那是等待.它异步等待结果.

<块引用>

过度使用 await/async 是否是您 Web 开发或 Angular 之类的需要?

这是为了管理延迟.

<块引用>

使每一行异步如何提高性能?

有两种方式.首先,通过确保应用程序在高延迟操作的世界中保持响应.这种性能对于不希望应用程序挂起的用户很重要.其次,通过为开发者提供工具来表达异步工作流中的数据依赖关系.通过不阻塞高延迟操作,可以释放系统资源以处理未阻塞的操作.

<块引用>

对我来说,它会因为启动所有这些线程而降低性能,不是吗?

没有线程.并发是一种实现异步的机制;它不是唯一的.

<块引用>

好的,所以如果我写这样的代码:await someMethod1();等待 someMethod2();等待 someMethod3();这会神奇地使应用程序更具响应性吗?

与什么相比更灵敏?与调用这些方法而不等待它们相比?不,当然不.与同步等待任务完成相比?绝对,是的.

<块引用>

我猜这就是我没有得到的.如果您在最后等待所有 3 个方法,那么是的,您正在并行运行 3 个方法.

不不不.停止考虑并行性.不需要任何并行性.

这样想.你想做一个煎鸡蛋三明治.您有以下任务:

  • 煎鸡蛋
  • 烤面包
  • 组装三明治

三个任务.第三个任务依赖于前两个的结果,但前两个任务不相互依赖.因此,这里有一些工作流程:

  • 在平底锅里放一个鸡蛋.煎鸡蛋时,盯着鸡蛋看.
  • 鸡蛋煮熟后,将一些吐司放入烤面包机.盯着烤面包机.
  • 烤好后,将鸡蛋放在吐司上.

问题是您可能在煮鸡蛋的同时将吐司放入烤面包机.替代工作流程:

  • 在平底锅里放一个鸡蛋.设置一个闹钟,当鸡蛋吃完时响起.
  • 将吐司放入烤面包机.设置一个闹钟,当烤面包完成时响起.
  • 检查您的邮件.做你的税.擦亮银器.无论您需要做什么.
  • 当两个闹钟都响起时,拿起鸡蛋和吐司,把它们放在一起,你就有了一个三明治.

您明白为什么异步工作流程效率更高了吗?在等待高延迟操作完成时,您会完成很多工作.但是您没有聘请鸡蛋厨师和吐司厨师.没有新话题!

我提议的工作流程是:

eggtask = FryEggAsync();toasttask = MakeToastAsync();蛋 = 等待蛋任务;toast = 等待 toasttask;返回制作三明治(鸡蛋,吐司);

现在,将其与:

eggtask = FryEggAsync();蛋 = 等待蛋任务;toasttask = MakeToastAsync();toast = 等待 toasttask;返回制作三明治(鸡蛋,吐司);

您是否看到该工作流程有何不同?此工作流程是:

  • 将鸡蛋放入锅中并设置闹钟.
  • 去做其他工作,直到闹钟响起.
  • 把鸡蛋从锅里拿出来;将面包放入烤面包机.设置闹钟...
  • 去做其他工作,直到闹钟响起.
  • 当闹钟响起时,组装三明治.

此工作流程效率较低因为我们未能捕捉到 toast 和 egg 任务具有高延迟和独立性这一事实.但这肯定比在等待鸡蛋煮熟时什么都不做更有效地利用资源.

整个事情的重点是:线程非常昂贵,所以不要启动新线程.相反,通过在执行高延迟操作时让线程工作来更有效地利用它.Await 不是关于启动新线程;这是关于在具有高延迟计算的世界中在一个线程上完成更多工作.

也许那个计算是在另一个线程上完成的,也许它在磁盘上被阻塞了,无论如何.没关系.关键是,await 是为了管理这种异步性,而不是创造它.

<块引用>

我很难理解如何在不使用并行的情况下实现异步编程.就像,你如何告诉程序在等待鸡蛋的同时开始烤面包,而没有 DoEggs() 同时运行,至少在内部?

回到类比.您正在制作鸡蛋三明治,鸡蛋和吐司正在烹饪,因此您开始阅读邮件.鸡蛋煮好后,邮件就完成了一半,因此您将邮件放在一边,然后将鸡蛋从火上移开.然后你回到邮件.然后吐司做好了,你做三明治.然后在三明治做好后阅读邮件.你是如何在不雇佣员工的情况下做到这一切的?一个人阅读邮件,一个人煮鸡蛋,一个人做吐司,一个人组装三明治?你用一个人完成了这一切工人.

你是怎么做到的?通过将任务分解成小块,注意哪些部分必须按什么顺序完成,然后协同处理这些部分.

如今拥有大型平面虚拟内存模型和多线程进程的孩子们认为这就是它一直以来的样子,但我的记忆可以追溯到 Windows 3 的时代,当时没有这些.如果您希望两件事并行"发生,那就是您所做的:将任务分成小部分并轮流执行部分.整个操作系统都是基于这个概念.

现在,您可能会看这个类比并说好吧,但是有些工作,例如实际敬酒,是由机器完成的",而就是并行性的来源.当然,我不必雇佣工人来烤面包,但我在硬件上实现了并行性.这是正确的思考方式.硬件并行和线程并行是不同的.当您向网络子系统发出异步请求以从数据库中查找记录时,没有线程坐在那里等待结果.硬件在远低于操作系统线程的水平上实现并行性.

如果您想更详细地了解硬件如何与操作系统配合以实现异步,请阅读没有主题" by Stephen Cleary.

所以当你看到async"时不要认为parallel".考虑将高延迟操作分成小块" 如果有许多这样的操作,它们的部分不相互依赖,那么您可以协同交错这些部分的执行一个线程.

正如您想象的那样,编写控制流是非常困难,您可以在其中放弃当前正在做的事情,去做其他事情,并无缝地从上次中断的地方继续.这就是为什么我们让编译器完成这项工作!await"的意义在于它允许您通过将这些异步工作流描述为同步工作流来管理它们.在任何地方,您都可以将此任务放在一边,稍后再回来处理,请写上等待".编译器会负责将您的代码变成许多小片段,每个片段都可以在异步工作流中进行调度.

更新:

<块引用>

在你的最后一个例子中,

<小时>

eggtask = FryEggAsync();蛋 = 等待蛋任务;toasttask = MakeToastAsync();toast = 等待 toasttask;

<小时>

egg = await FryEggAsync();toast = await MakeToastAsync();?

<小时><块引用>

我假设它同步调用它们但异步执行它们?我不得不承认,我以前从来没有费心去单独等待任务.

没有区别.

FryEggAsync被调用时,不管await是否出现在它前面,它都会被调用.await 是一个 运算符.它对调用 FryEggAsync返回的事物进行操作.就像任何其他运算符一样.

让我再说一遍:await 是一个 运算符,它的操作数是一个任务.可以肯定的是,这是一个非常不寻常的运算符,但在语法上它是一个运算符,并且它像任何其他运算符一样对 进行操作.

让我再说一遍:await 不是您在调用站点上放置的魔法灰尘,并且突然该调用站点被远程连接到另一个线程.当调用发生时调用发生,调用返回一个,该值是对一个对象的引用,该对象是await运算符的合法操作数.

是的,

var x = Foo();var y = 等待 x;

var y = await Foo();

是一样的东西,一样的

var x = Foo();变量 y = 1 + x;

var y = 1 + Foo();

都是一样的.

所以让我们再讲一遍,因为您似乎相信await 导致异步的神话.没有.

异步任务 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 运算符的目的是使用同步工作流的编码约定来构建异步工作流.所以要考虑的是那个工作流程是什么?工作流对一组相关任务进行排序.

查看工作流中所需排序的最简单方法是检查数据依赖性.在吐司从烤面包机中出来之前,您无法制作三明治,因此您将不得不某处获得吐司.由于 await 从已完成的任务中提取值,因此在创建烤面包机任务和创建三明治之间必须有一个 await 某处.

您还可以表示对副作用的依赖.比如用户按下按钮,那么你要播放警笛声,然后等待三秒,然后开门,然后等待三秒,然后关门:

DisableButton();播放警报器();等待 Task.Delay(3000);开门();等待 Task.Delay(3000);关门();启用按钮();

说是没有意义的

DisableButton();播放警报器();var delay1 = Task.Delay(3000);开门();var delay2 = Task.Delay(3000);关门();启用按钮();等待延迟1;等待延迟2;

因为这不是所需的工作流程.

因此,您问题的实际答案是:将 await 推迟到实际需要该值的时间点是一个很好的做法,因为它增加了有效安排工作的机会.但是你可以走得太远;确保实施的工作流程是您想要的工作流程.

I come from a back end / thick client background, so maybe I'm missing something... but I recently looked at the source for an open source JWT token server and the authors went crazy with await / async. Like on every method and every line.

I get what the pattern is for... to run long running tasks in a separate thread. In my thick client days, I would use it if a method might take a few seconds, so as not to block the GUI thread... but definitely not on a method that takes a few ms.

Is this excessive use of await / async something you need for web dev or for something like Angular? This was in a JWT token server, so not even seeing what it has to do with any of those. It's just a REST end point.

How is making every single line async going to improve performace? To me, it'll kill performance from spinning up all those threads, no?

解决方案

I get what the pattern is for... to run long running tasks in a separate thread.

This is absolutely not what this pattern is for.

Await does not put the operation on a new thread. Make sure that is very clear to you. Await schedules the remaining work as the continuation of the high latency operation.

Await does not make a synchronous operation into an asynchronous concurrent operation. Await enables programmers who are working with a model that is already asynchronous to write their logic to resemble synchronous workflows. Await neither creates nor destroys asynchrony; it manages existing asynchrony.

Spinning up a new thread is like hiring a worker. When you await a task, you are not hiring a worker to do that task. You are asking "is this task already done? If not, call me back when its done so I can keep doing work that depends on that task. In the meanwhile, I'm going to go work on this other thing over here..."

If you're doing your taxes and you find you need a number from your work, and the mail hasn't arrived yet, you don't hire a worker to wait by the mailbox. You make a note of where you were in your taxes, go get other stuff done, and when the mail comes, you pick up where you left off. That's await. It's asynchronously waiting for a result.

Is this excessive use of await / async something you need for web dev or for something like Angular?

It's to manage latency.

How is making every single line async going to improve performance?

In two ways. First, by ensuring that applications remain responsive in a world with high-latency operations. That kind of performance is important to users who don't want their apps to hang. Second, by providing developers with tools for expressing the data dependency relationships in asynchronous workflows. By not blocking on high-latency operations, system resources are freed up to work on unblocked operations.

To me, it'll kill performance from spinning up all those threads, no?

There are no threads. Concurrency is a mechanism for achieving asynchrony; it is not the only one.

Ok, so if I write code like: await someMethod1(); await someMethod2(); await someMethod3(); that is magically going to make the app more responsive?

More responsive compared to what? Compared to calling those methods without awaiting them? No, of course not. Compared to synchronously waiting for the tasks to complete? Absolutely, yes.

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.

No no no. Stop thinking about parallelism. There need not be any parallelism.

Think about it this way. You wish to make a fried egg sandwich. You have the following tasks:

  • Fry an egg
  • Toast some bread
  • Assemble a sandwich

Three tasks. The third task depends on the results of the first two, but the first two tasks do not depend on each other. So, here are some workflows:

  • Put an egg in the pan. While the egg is frying, stare at the egg.
  • Once the egg is done, put some toast in the toaster. Stare at the toaster.
  • Once the toast is done, put the egg on the toast.

The problem is that you could be putting the toast in the toaster while the egg is cooking. Alternative workflow:

  • Put an egg in the pan. Set an alarm that rings when the egg is done.
  • Put toast in the toaster. Set an alarm that rings when the toast is done.
  • Check your mail. Do your taxes. Polish the silverware. Whatever it is you need to do.
  • When both alarms have rung, grab the egg and the toast, put them together, and you have a sandwich.

Do you see why the asynchronous workflow is far more efficient? You get lots of stuff done while you're waiting for the high latency operation to complete. But you did not hire an egg chef and a toast chef. There are no new threads!

The workflow I proposed would be:

eggtask = FryEggAsync();
toasttask = MakeToastAsync();
egg = await eggtask;
toast = await toasttask;
return MakeSandwich(egg, toast);

Now, compare that to:

eggtask = FryEggAsync();
egg = await eggtask;
toasttask = MakeToastAsync();
toast = await toasttask;
return MakeSandwich(egg, toast);

Do you see how that workflow differs? This workflow is:

  • Put an egg in the pan and set an alarm.
  • Go do other work until the alarm goes off.
  • Get the egg out of the pan; put the bread in the toaster. Set an alarm...
  • Go do other work until the alarm goes off.
  • When the alarm goes off, assemble the sandwich.

This workflow is less efficient because we have failed to capture the fact that the toast and egg tasks are high latency and independent. But it is surely more efficient use of resources than doing nothing while you're waiting for the egg to cook.

The point of this whole thing is: threads are insanely expensive, so don't spin up new threads. Rather, make more efficient use of the thread you've got by putting it to work while you're doing high latency operations. Await is not about spinning up new threads; it is about getting more work done on one thread in a world with high latency computation.

Maybe that computation is being done on another thread, maybe it's blocked on disk, whatever. Doesn't matter. The point is, await is for managing that asynchrony, not creating it.

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?

Go back to the analogy. You are making an egg sandwich, the eggs and toast are cooking, and so you start reading your mail. You get halfway through the mail when the eggs are done, so you put the mail aside and take the egg off the heat. Then you go back to the mail. Then the toast is done and you make the sandwich. Then you finish reading your mail after the sandwich is made. How did you do all that without hiring staff, one person to read the mail, one person to cook the egg, one to make the toast and one to assemble the sandwich? You did it all with a single worker.

How did you do that? By breaking tasks up into small pieces, noting which pieces have to be done in which order, and then cooperatively multitasking the pieces.

Kids today with their big flat virtual memory models and multithreaded processes think that this is how its always been, but my memory stretches back to the days of Windows 3, which had none of that. If you wanted two things to happen "in parallel" that's what you did: split the tasks up into small parts and took turns executing parts. The whole operating system was based on this concept.

Now, you might look at the analogy and say "OK, but some of the work, like actually toasting the toast, is being done by a machine", and that is the source of parallelism. Sure, I didn't have to hire a worker to toast the bread, but I achieved parallelism in hardware. And that is the right way to think of it. Hardware parallelism and thread parallelism are different. When you make an asynchronous request to the network subsystem to go find you a record from a database, there is no thread that is sitting there waiting for the result. The hardware achieves parallelism at a level far, far below that of operating system threads.

If you want a more detailed explanation of how hardware works with the operating system to achieve asynchrony, read "There is no thread" by Stephen Cleary.

So when you see "async" do not think "parallel". Think "high latency operation split up into small pieces" If there are many such operations whose pieces do not depend on each other then you can cooperatively interleave the execution of those pieces on one thread.

As you might imagine, it is very difficult to write control flows where you can abandon what you are doing right now, go do something else, and seamlessly pick up where you left off. That's why we make the compiler do that work! The point of "await" is that it lets you manage those asynchronous workflows by describing them as synchronous workflows. Everywhere that there is a point where you could put this task aside and come back to it later, write "await". The compiler will take care of turning your code into many tiny pieces that can each be scheduled in an asynchronous workflow.

UPDATE:

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.

There is no difference.

When FryEggAsync is called, it is called regardless of whether await appears before it or not. await is an operator. It operates on the thing returned from the call to FryEggAsync. It's just like any other operator.

Let me say this again: await is an operator and its operand is a task. It is a very unusual operator, to be sure, but grammatically it is an operator, and it operates on a value just like any other operator.

Let me say it again: await is not magic dust that you put on a call site and suddenly that call site is remoted to another thread. The call happens when the call happens, the call returns a value, and that value is a reference to an object that is a legal operand to the await operator.

So yes,

var x = Foo();
var y = await x;

and

var y = await Foo();

are the same thing, the same as

var x = Foo();
var y = 1 + x;

and

var y = 1 + Foo();

are the same thing.

So let's go through this one more time, because you seem to believe the myth that await causes asynchrony. It does not.

async Task M() { 
   var eggtask = FryEggAsync(); 

Suppose M() is called. FryEggAsync is called. Synchronously. There is no such thing as an asynchronous call; you see a call, control passes to the callee until the callee returns. The callee returns a task which represents an egg to be made available in the future.

How does FryEggAsync do this? I don't know and I don't care. All I know is I call it, and I get an object back that represents a future value. Maybe that value is produced on a different thread. Maybe it is produced on this thread but in the future. Maybe it is produced by special-purpose hardware, like a disk controller or a network card. I don't care. I care that I get back a task.

  egg = await eggtask; 

Now we take that task and await asks it "are you done?" If the answer is yes, then egg is given the value produced by the task. If the answer is no then M() returns a Task representing "the work of M will be completed in the future". The remainder of M() is signed up as the continuation of eggtask, so when eggtask completes, it will call M() again and pick it up not from the beginning, but from the assignment to egg. M() is a resumable at any point method. The compiler does the necessary magic to make that happen.

So now we've returned. The thread keeps on doing whatever it does. At some point the egg is ready, so the continuation of eggtask is invoked, which causes M() to be called again. It resumes at the point where it left off: assigning the just-produced egg to egg. And now we keep on trucking:

toasttask = MakeToastAsync(); 

Again, the call returns a task, and we:

toast = await toasttask; 

check to see if the task is complete. If yes, we assign toast. If no, then we return from M() again, and the continuation of toasttask is *the remainder of M().

And so on.

Eliminating the task variables does nothing germane. Storage for the values is allocated; it's just not given a name.

ANOTHER UPDATE:

is there a case to be made to call Task-returning methods as early as possible but awaiting them as late as possible?

The example given is something like:

var task = FooAsync();
DoSomethingElse();
var foo = await task;
...

There is some case to be made for that. But let's take a step back here. The purpose of the await operator is to construct an asynchronous workflow using the coding conventions of a synchronous workflow. So the thing to think about is what is that workflow? A workflow imposes an ordering upon a set of related tasks.

The easiest way to see the ordering required in a workflow is to examine the data dependence. You can't make the sandwich before the toast comes out of the toaster, so you're going to have to obtain the toast somewhere. Since await extracts the value from the completed task, there's got to be an await somewhere between the creation of the toaster task and the creation of the sandwich.

You can also represent dependencies on side effects. For example, the user presses the button, so you want to play the siren sound, then wait three seconds, then open the door, then wait three seconds, then close the door:

DisableButton();
PlaySiren();
await Task.Delay(3000);
OpenDoor();
await Task.Delay(3000);
CloseDoor();
EnableButton();

It would make no sense at all to say

DisableButton();
PlaySiren();
var delay1 = Task.Delay(3000);
OpenDoor();
var delay2 = Task.Delay(3000);
CloseDoor();
EnableButton();
await delay1;
await delay2;

Because this is not the desired workflow.

So, the actual answer to your question is: deferring the await until the point where the value is actually needed is a pretty good practice, because it increases the opportunities for work to be scheduled efficiently. But you can go too far; make sure that the workflow that is implemented is the workflow you want.

这篇关于为什么 Web 应用程序现在对 await/async 变得疯狂?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆