为什么当今的Web应用程序会因等待/异步而发疯? [英] Why are web apps going crazy with await / async nowadays?

查看:53
本文介绍了为什么当今的Web应用程序会因等待/异步而发疯?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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.

我了解到该模式的用途是...在单独的线程中运行长时间运行的任务.在忙碌的客户中,如果某个方法可能需要几秒钟的时间,我会使用它,以免阻塞GUI线程...但绝对不会在需要几毫秒的方法上使用.

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.

这种过度使用await/async是Web开发或Angular需要的东西吗?这是在JWT令牌服务器中,因此甚至看不到它与这些令牌有什么关系.这只是REST的终点.

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.

这绝对不是此模式的目的.

等待将操作放到新线程上.确保您非常清楚. 等待将剩余工作安排为高延迟操作的延续.

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不会 not 将同步操作转换为异步并发操作. 等待使正在使用异步模型的程序员能够编写类似于同步工作流的逻辑.等待既不会创造也不会破坏异步.它管理现有的异步.

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.

这种过度使用await/async是Web开发或Angular等所需的东西吗?

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?

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

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.

好吧,所以如果我写这样的代码:await someMethod1();等待someMethod2();等待someMethod3();那将使应用程序具有更快的响应速度吗?

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.

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

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:

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

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

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:

  • 在锅里放一个鸡蛋.鸡蛋煎炸时,凝视鸡蛋.
  • 鸡蛋煮好后,在烤面包机中放些烤面包.盯着烤面包机.
  • 完成烤面包后,将鸡蛋放到烤面包上.

问题在于您可能会在煮鸡蛋的同时将烤面包片放入烤面包机中.替代工作流程:

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

  • 在锅里放一个鸡蛋.设置一个在鸡蛋煮完时响起的警报.
  • 将吐司面包放入烤面包机中.设置一个在烤面包结束时响起的警报.
  • 检查您的邮件.做你的税.擦亮银器.无论您需要做什么.
  • 当两个警报响起时,抓住鸡蛋和吐司,将它们放在一起,就可以得到一个三明治.

您看到异步工作流为何效率更高的原因吗?等待高延迟操作完成时,您会完成很多工作. 但是您没有雇用鸡蛋厨师和烤面包厨师.没有新线程!

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!

我建议的工作流程为:

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);

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

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

  • 在锅里放一个鸡蛋并设置一个闹钟.
  • 去做其他工作,直到警报响起.
  • 把鸡蛋从锅里拿出来;把面包放进烤面包机.设置闹钟...
  • 去做其他工作,直到警报响起为止.
  • 警报响起时,组装三明治.

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

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.

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

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.

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

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.

您可能想象到,编写控制流在这里非常困难,您可以在其中放弃当前正在做的事情,去做其他事情,然后无缝地从上次中断的地方接管.这就是为什么我们让编译器完成这项工作! 等待"的要点是,您可以通过将它们描述为同步工作流来管理这些异步工作流.到处都可以将任务搁置一旁并稍后再返回,请写下"await".编译器将负责将您的代码分解成许多细小的片段,每个片段都可以在异步工作流中安排.

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.

更新:

在您的最后一个示例中,两者之间有什么区别

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返回进行操作.就像其他运算符一样.

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.

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

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.

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

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.

是的,

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

var y = await Foo();

是同一件事,与

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

var y = 1 + Foo();

是同一回事.

因此,让我们再经历一次,因为您似乎相信await 引起异步的神话.不会.

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(); 

假设M()被调用. 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.

FryEggAsync如何执行此操作?我不知道,我不在乎.我所知道的就是我称之为它,并且我得到了一个代表未来价值的物体.也许该值是在另一个线程上产生的.可能是在此线程上生成的,但将来可能在 上生成.可能是由专用硬件生产的,例如磁盘控制器或网卡.我不在乎我很想找回任务.

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; 

现在我们执行该任务,然后await询问它您完成了吗?"如果答案为是,则为egg提供任务产生的值.如果答案为否,则M()返回Task,表示"M的工作将在将来完成". M()的其余部分被标记为eggtask的延续,因此eggtask完成后,它将再次调用M()并从 而不是从拾取它. em> egg 的分配. M()是任意时间的可恢复方法.编译器做了必要的魔术才能做到这一点.

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.

现在我们回来了.线程继续做任何事情.在某个时候鸡蛋已经准备好,因此eggtask的延续被调用,这导致M()被再次调用.它从中断的位置恢复:将刚生产的鸡蛋分配给egg.现在我们继续卡车运输:

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; 

检查任务是否完成.如果是,我们分配toast.如果否,则我们再次从M()返回,而toasttask continuation 是*(M)的余数.

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().

以此类推.

消除task变量没有任何关联.分配值的存储空间;只是没有名字.

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

另一个更新:

是否有必要尽早调用任务返回方法,但要尽可能晚地等待它们?

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;
...

要为此准备 some 个案例.但是,让我们退后一步. await运算符的目的是使用同步工作流的编码约定来构建异步工作流.所以要考虑的是那是什么工作流程? 工作流程对一组相关任务强加了顺序.

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.

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

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();

说毫无意义

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应用程序会因等待/异步而发疯?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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