为什么Web应用程序现在正在等待/asynchronous疯狂?

我来自后端/厚客户端的背景,所以也许我错过了一些东西……但我最近看了一个开源JWT令牌服务器的源代码,作者疯狂地等待/asynchronous。 就像每一个方法和每一行一样。

我得到的模式是…在一个单独的线程中运行长时间运行的任务。 在我密集的客户日子里,如果一个方法可能需要几秒钟,我会使用它,以免阻塞GUI线程…但绝对不是一个需要几ms的方法。

这是过度使用等待/asynchronous的东西你需要的Web开发或像Angular的东西? 这是在一个JWT令牌服务器,所以甚至没有看到它与任何这些。 这只是一个REST的终点。

如何使每一行asynchronous改善性能? 对我来说,它会杀死所有这些线程的performance,不是吗?

我得到的模式是…在一个单独的线程中运行长时间运行的任务。

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

等待不会把操作放到新的线程上。 确保你清楚。 等待将剩余的工作安排为高延迟操作的延续。

等待不会进行asynchronous并发操作的同步操作。 等待使程序员能够使用已经是asynchronous的模型来编写他们的逻辑来模拟同步工作stream程 。 等待既不创造也不破坏asynchronous; 它pipe理现有的asynchronous。

纺织一个新的线索就像雇用一名工人。 当你等待一项任务时,你不会雇用一名工人来完成这项任务。 你在问:“这个任务已经完成了吗?如果没有,完成后再打给我,我可以继续做这个任务,同时,我要在这里继续工作。 “。

如果你正在征税,而你发现自己的工作需要一个号码,而邮件还没有到达,你就不会雇用一名工人在邮箱旁边等候 。 你记下你在税收上的位置,完成其他的工作,当邮件到达时,你就可以从你离开的地方继续。 这是等待 。 这是asynchronous等待结果

这是过度使用等待/asynchronous的东西你需要的Web开发或像Angular的东西?

这是pipe理延迟。

如何使每一行asynchronous将提高性能?

有两种方法。 首先,确保应用程序在具有高延迟操作的世界中保持响应。 这种性能对不希望自己的应用挂起的用户很重要。 其次,通过为开发人员提供在asynchronous工作stream程中expression数据依赖关系的工具。 通过不阻塞高延迟操作,系统资源被释放,以解除阻塞的操作。

对我来说,它会杀死所有这些线程的performance,不是吗?

没有线程。 并发是实现asynchronous的机制; 它不是唯一的一个。

好的,所以如果我写代码:await someMethod1(); 等待someMethod2(); 等待someMethod3(); 这是奇迹般的将使应用程序更具响应性?

比什么更响应? 相比于没有等待他们调用这些方法? 不,当然不。 比较同步等待任务完成? 绝对没错。

这是我没有得到我猜。 如果你最后3等待,那么是的,你并行运行3种方法。

不不不。 不要再想并行。 不需要任何并行性。

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

  • 炒鸡蛋
  • 吐司面包
  • 组装一个三明治

三个任务。 第三个任务取决于前两个的结果,但前两个任务不相互依赖。 所以,这里有一些工作stream程:

  • 把一个鸡蛋放在锅里。 当鸡蛋在煎,盯着鸡蛋。
  • 一旦鸡蛋完成,在烤面包机上放一些烤面包片。 盯着烤面包机。
  • 烤面包完成后,把鸡蛋放在面包上。

问题是,当鸡蛋正在烹饪时,你可以把面包放在烤面包机上。 备选工作stream程

  • 把一个鸡蛋放在锅里。 设置一个在蛋完成时响起的警报。
  • 把烤面包片放在烤面包机上。 设置敬酒完成时响起的闹铃。
  • 请查收你的邮件。 做你的税。 擦亮银器。 无论你需要做什么
  • 当两个警报响起时,抓住鸡蛋和烤面包,把它们放在一起,你有一个三明治。

你明白为什么asynchronous工作stream程更有效率吗? 在等待高延迟操作完成时,您会完成很多任务。 但是你没有雇用一个鸡蛋厨师和一个烤面包师傅 。 没有新的主题!

我提出的工作stream程是:

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

你看到这个工作stream程有什么不同吗? 这个工作stream程是:

  • 把一个鸡蛋放在锅里,并设置一个警报。
  • 去做其他的工作,直到闹钟响了。
  • 把蛋从锅里拿出来; 把面包放在烤面包机里。 设置闹钟…
  • 去做其他的工作,直到闹钟响了。
  • 当警报熄灭时,组装三明治。

这个工作stream效率不高, 因为我们没有捕捉到吐司和蛋任务是高延迟和独立的事实 。 但是当你在等待鸡蛋煮饭时,肯定会更有效地利用资源。

这整个事情的重点是:线程非常昂贵,所以不要启动新线程。 而是通过在执行高延迟操作时使其工作从而更有效地利用所获得的线程 。 等待不是关于转动新线程; 它是在一个具有高延迟计算的世界中的一个线程上完成更多的工作。

也许这个计算是在另一个线程上完成的,也许是在磁盘上被阻塞了,不pipe怎样。 没关系。 关键是,等待是pipe理asynchronous,而不是创build它。

我很难理解如何在不使用并行机制的情况下实现asynchronous编程。 比如,你如何告诉程序开始在没有同时运行DoEggs()的情况下等待鸡蛋,至less在内部?

回到这个比喻。 你正在做一个鸡蛋三明治,鸡蛋和吐司正在烹饪,所以你开始阅读你的邮件。 当鸡蛋完成后,你会收到邮件的一半,所以你把邮件放在一边,把鸡蛋放热。 然后你回到邮件。 然后吐司完成,你做三明治。 然后你完成三明治制作后阅读你的邮件。 你是怎么做的,不用雇用员工,一个人读邮件,一个人做蛋,一个做面包,一个做三明治? 你用一名工人完成了这一切。

你是怎么做到的? 通过把任务分解成小块,注意哪些块需要按照哪个顺序来完成,然后合作地对这些块进行多任务处理

现在的孩子们,他们的平板虚拟内存模型和multithreading进程认为,这是如何一直如此,但我的记忆可以追溯到Windows 3的时代,而这一切都没有。 如果你想要两件事情“并行”发生,就是你所做的:把任务分成小部分,轮stream执行部分。 整个操作系统是基于这个概念的。

现在,你可以看看这个比喻,说“好吧,但是其中的一些工作,比如说敬酒敬酒,是由一台机器完成的”, 就是并行的来源。 当然,我不必雇用一个工人来烤面包,但是我在硬件上达到了平行。 这是正确的思路。 硬件并行性和线程并行性是不同的 。 当您向networking子系统发出一个asynchronous请求,从数据库中find一条logging时, 没有线程正坐在那里等待结果。 硬件在远远低于操作系统线程的水平上实现了并行性。

如果您想要更详细地解释硬件如何在操作系统中实现asynchronous,请阅读Stephen Cleary的“ 没有线程 ”。

所以当你看到“asynchronous”时,不要想“平行”。 把“高延时操作分解成小块”如果有很多这样的操作,它们不相互依赖,那么你可以在一个线程上协同交错执行这些块。

正如你可能想象的那样,编写控制stream程是非常困难的 ,你可以放弃你现在正在做的事情,去做别的事情,然后无缝地继续下去。 这就是为什么我们让编译器做这个工作! “等待”的意义在于它允许您通过将它们描述为同步工作stream来pipe理这些asynchronous工作stream程。 在任何地方,你可以放下这个任务,稍后再回来,写下“等待”。 编译器会把你的代码转换成许多小块,每个小块都可以在asynchronous工作stream中进行调度。

更新:

在你最后的例子中,两者之间会有什么区别


 eggtask = FryEggAsync(); egg = await eggtask; toasttask = MakeToastAsync(); toast = await toasttask; 

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

我假设它同步调用它们,但asynchronous执行它们? 我不得不承认,我从来没有打扰过要单独等待这个任务。

没有区别。

FryEggAsync ,不pipeawait是否显示,都会调用它。 await是一个操作员 。 它对从FryEggAsync调用返回的东西进行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 导致asynchronous的神话。 它不是。

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

假设M()被调用。 FryEggAsync被调用。 同步。 没有asynchronous调用这样的事情; 你看到一个呼叫,控制权传递给被调用者,直到被调用者返回。 被调用者返回一个代表将来可用的鸡蛋的任务

FryEggAsync如何做到这一点? 我不知道,我不在乎。 我所知道的是我所说的,并且我得到一个代表未来价值的物体。 也许这个价值是在不同的线程上产生的。 也许它是在这个线程上产生,但在未来 。 也许它是由专用硬件产生的,如磁盘控制器或网卡。 我不在乎。 我在意我回来了一个任务。

  egg = await eggtask; 

现在我们接下这个任务, await问“你做完了吗?” 如果答案是肯定的,那么egg会得到任务产生的价值。 如果答案是否定的,则M()返回一个代表“M的工作将在未来完成”的任务。 M()的剩余部分被注册为eggtask的延续,所以当eggtask完成时,它会再次调用M() ,并且不是从开始就把它提取出来 ,而是从赋值到egg 。 M() 在任何一点方法都是可恢复的 。 编译器做了必要的魔术来实现这一点。

所以现在我们已经回来了。 线程继续做任何事情。 在某一时刻,蛋已经准备好了,所以eggtask的继续被调用,这导致M()被再次调用。 它在离开的地方重新开始:将刚生成的鸡蛋分配给egg 。 现在我们继续卡车:

 toasttask = MakeToastAsync(); 

再次,这个调用返回一个任务,我们:

 toast = await toasttask; 

检查任务是否完成。 如果是,我们分配toast 。 如果不是,那么我们从M()返回 ,而toasttask继续是M()的余数。

等等。

消除taskvariables没有任何密切关系。 存储的价值分配; 它只是没有一个名字。

其他更新:

有没有办法尽早调用任务返回方法,但是尽可能晚地等待呢?

给出的例子是这样的:

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

一些情况下可以这样做。 但是让我们回到这里。 await操作符的用途是使用同步工作stream的编码约定来构造asynchronous工作stream 。 所以要考虑的是这个工作stream程什么工作stream程会对一组相关任务进行sorting。

查看工作stream程中所需顺序的最简单方法是检查数据依赖性 。 烤面包机出炉之前,你不能做三明治,所以你得在某个地方得到烤面包。 由于等待从完成的任务中提取价值,所以在创build烤面包机任务和创build三明治之间需要等待一段时间。

您也可以表示对副作用的依赖关系。 例如,用户按下button,所以要播放警笛声,然后等待三秒钟,然后打开门,然后等待三秒钟,然后关上门:

 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; 

因为这不是所需的工作stream程。

所以,你的问题的实际答案是:推迟等待,直到实际需要的价值是一个非常好的做法,因为它增加了工作的机会有效地安排。 但是你可以走得太远; 确保所实施的工作stream程是您想要的工作stream程。

一般来说,这是因为一旦asynchronous函数与其他asynchronous函数一起发挥更好,否则你开始失去asynchronous性的好处。 因此,调用asynchronous函数的函数本身就是asynchronous的,并在整个应用程序中传播。 如果您使用asynchronous数据存储进行交互,那么使用该function的东西往往也会变得asynchronous。

在将同步代码转换为asynchronous代码时,您会发现如果asynchronous代码调用并被其他asynchronous代码调用,则一切顺利(如果您愿意,可以向上调整),效果最好。 其他人也注意到了asynchronous编程的传播行为,并称之为“传染性”,或者将其与僵尸病毒进行比较。 无论是海龟还是僵尸,asynchronous代码往往会驱动周围的代码也是asynchronous的。 这种行为在所有types的asynchronous编程中都是固有的,而不仅仅是新的async / await关键字。

来源: Async / Await – asynchronous编程的最佳实践

这是一个演员模特世界真的…

我的观点是,asynchronous/等待只是一种打磨软件系统的方式,以避免不得不承认,很多系统(特别是那些有很多networking通信的系统)更好地被视为Actor模型(或更好的然而,沟通顺序过程)系统。

这两点都意味着你需要等待几件事情中的一件完成,当有的时候采取必要的行动,然后再等待。 具体而言,您正在等待从其他地方发送消息,阅读消息并根据内容采取行动。 在* nix中,等待通常是通过调用epoll()或select()来完成的。

使用await / async只是假装你的系统仍然是一种同步的方法调用(并且因此是熟悉的)的一种方式,同时使得难以有效地处理每次不一致地以相同的顺序完成的事情。

然而,一旦你了解到你不再调用方法,而是简单地传递消息,这一切就变得非常自然了。 这是一个“请做这个”,“当然,这是答案”的东西,许多这样的相互作用交织在一起。 在循环顶部用一个大的WaitForLotsOfThings()调用来包装它,仅仅是一个明确的确认,你的程序将等待,直到它响应许多其他与之通信的程序。

Windows如何使它变得艰难

不幸的是,Windows使得实现proactor系统非常困难(“如果你现在阅读这个消息,你会得到它”)。 Windows是反应堆(“你要求我读的那条消息?现在已经被读了。”)。 这是一个重要的区别。

对于前者,意味着“停止倾听其他演员”的信息(或确实是超时)很容易处理 – 您只需在下次等待时从列表中排除其他演员。

有了反应堆,情况就更难了。 当阅读已经开始进行某种asynchronous调用时,如何才能够“停止聆听另一个演员”的消息,直到读到某个东西才会完成,这是最近收到的指令的可疑结果?

我在某种程度上挑剔。 Proactor在具有dynamic连接的系统中非常有用,Actor落入系统,再次辍学。 反应堆没有问题,如果你有一个固定的演员与通信链接永远不会消失。 尽pipe如此,由于反应堆系统易于在前期处理平台上实施,但前期处理系统不能轻易地在反应堆平台上实施(时间不会倒退),所以我觉得Window的处理方法特别刺激。

所以不pipe怎么样,asynchronous/等待肯定还是在反应堆的土地上。

敲击影响

这已经感染了许多其他图书馆。

C ++的Boost asio也是一个反应器,甚至在* nix上,似乎是因为他们想要一个Windows实现。

ZeroMQ是一个前置机制框架,它在一定程度上受限于基于对select()的调用(在Windows中只适用于套接字)。

对于Windows上的POSIX运行时的cygwin系列,他们必须通过为每个文件描述符轮询 (是, polling !!!!)实现select(),epoll()等,以便底层套接字/串行端口/pipe道传入数据以重新创buildPOSIX的例程。 Yeurk! cygwin dev的邮件列表上的评论可以追溯到他们实现该部分的时候,使得阅读有趣。

演员不一定慢

值得注意的是,短语“传递消息”并不一定意味着要传递副本 – 有很多Actor模型的expression式,你只是将引用的所有权传递给周围的消息(例如Dataflow,任务并行库的一部分C#)。 这使它快速。 我还没有仔细研究Dataflow库,但是它并没有真正使Windows proactor突然变得如此。 它不给你一个演员模型proactor系统,处理各种数据承载,如套接字,pipe道,队列等。

Windows 10的Linux运行时

因此,刚刚发布的Windows和反应器体系结构较差,有一个有趣的地方是Windows 10现在运行Linux二进制文件。 我非常想知道,微软是如何实现在select(),epoll()之下的系统调用,因为它必须在套接字,串口,pipe道以及POSIX领域的所有其他东西上运行,文件描述符,当在Windows上的一切都不能? 我想让我的后牙知道这个问题的答案。