现在为什么网络应用程序会因await/async而疯狂?

Sle*_*mer 44 c# multithreading asynchronous

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

我得到的模式是...在一个单独的线程中运行长时间运行的任务.在我很厚的客户端,我会使用它,如果一个方法可能需要几秒钟,以便不阻止GUI线程...但绝对不是一个需要几毫秒的方法.

这是否过度使用await/async你需要的web dev或类似Angular的东西?这是在JWT令牌服务器中,因此甚至没有看到它与任何这些服务器有什么关系.这只是一个REST终点.

如何让每一行异步都能提高性能?对我来说,它会破坏所有这些线程的性能,不是吗?

Eri*_*ert 132

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

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

等待并没有提上一个新的线程操作.确保你清楚. Await将剩余的工作安排为高延迟操作的延续.

等待并没有让一个同步操作为异步并行操作. Await使正在处理已经异步的模型的程序员能够将其逻辑编写为类似于同步工作流.等待既不创造也不破坏异步; 它管理现有的异步.

启动新线程就像雇佣一名工人一样.当您等待任务时,您不会雇佣工人来完成该任务.你问"这个任务已经完成了吗?如果没有,请在完成任务后给我回电话,这样我就可以继续做依赖于该任务的工作.同时,我将继续在这里做另外的工作.. ".

如果您正在缴税并且发现您的工作中需要一个号码,并且邮件还没有到达,那么您就不会雇佣一名工人来等待邮箱.你记下你在税收中的位置,去完成其他事情,当邮件到来时,你会从你离开的地方继续.那是等待的.它异步等待结果.

这是否过度使用await/async你需要的web dev或类似Angular的东西?

这是管理延迟.

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

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

对我来说,它会破坏所有这些线程的性能,不是吗?

没有主题.并发是一种实现异步的机制; 它不是唯一的一个.

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

相比什么更敏感?与在不等待它们的情况下调用这些方法相比?不,当然不.相比同步等待任务完成?绝对没错.

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

不不不.不要再考虑并行性了.不需要任何并行性.

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

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

三项任务.第三个任务取决于前两个任务的结果,但前两个任务不依赖于彼此.所以,这里有一些工作流程:

  • 把鸡蛋放在锅里.鸡蛋煎炸时,盯着鸡蛋.
  • 鸡蛋完成后,在烤面包机中放一些吐司.盯着烤面包机.
  • 烤面包完成后,将鸡蛋放在烤面包上.

问题是你可以在鸡蛋烹饪时把烤面​​包片放在烤面包机里.替代工作流程

  • 把鸡蛋放在锅里.设置鸡蛋完成时响铃的闹钟.
  • 在烤面包机烤面包.设置在吐司完成时响铃的闹钟.
  • 请查收你的邮件.你的税.抛光银器.无论你需要做什么.
  • 当两个警报响起时,抓住鸡蛋和烤面包,将它们放在一起,你就有一个三明治.

你明白为什么异步工作流程效率更高吗?在等待高延迟操作完成时,您可以完成许多工作. 但是你没有聘请一位蛋厨师和一名烤面包师.没有新的主题!

我提出的工作流程是:

eggtask = FryEggAsync();
toasttask = MakeToastAsync();
egg = await eggtask;
toast = await toasttask;
return MakeSandwich(egg, toast);
Run Code Online (Sandbox Code Playgroud)

现在,将其与:

eggtask = FryEggAsync();
egg = await eggtask;
toasttask = MakeToastAsync();
toast = await toasttask;
return MakeSandwich(egg, toast);
Run Code Online (Sandbox Code Playgroud)

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

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

这种工作流程的效率较低,因为我们未能捕捉到吐司和蛋任务具有高延迟和独立性这一事实.但是,当你在等蛋煮时,它肯定比使用任何东西更有效地利用资源.

这一切的重点是:线程非常昂贵,所以不要启动新线程.相反,在进行高延迟操作时,通过将其用于工作,可以更有效地使用您获得的线程.等待不是关于开辟新线程; 它是关于在具有高延迟计算的世界中在一个线程上完成更多工作.

也许这个计算是在另一个线程上完成的,也许它在磁盘上被阻止了,无论如何.无所谓.关键是,await用于管理异步,而不是创建它.

我很难理解如何在不使用并行性的情况下实现异步编程.比如,如果没有DoEggs()同时运行,至少在内部运行,你如何告诉程序开始吐司?

回到类比.你正在做一个鸡蛋三明治,鸡蛋和烤面包正在做饭,所以你开始阅读你的邮件.当鸡蛋完成后,你会收到邮件的中途,所以你把邮件放在一边,把鸡蛋取出来加热.然后你回到邮件.然后吐司完成,你做三明治.然后你在制作三明治后读完邮件.如果没有雇用员工,一个人阅读邮件,一个人做鸡蛋,一个人做吐司,一个人组装三明治,你是怎么做到的?你用一个工人完成了这一切.

你是怎么做到的?通过将任务分解成小块,注意哪些块必须以哪种顺序完成,然后协同地多任务处理碎片.

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

现在,你可以看一下这个比喻并说"好吧,但有些工作,比如实际上烘烤吐司,是由一台机器完成的",就是并行的源泉.当然,我没有雇佣一名工人来烤面包,但我在硬件上实现了并行性.这是思考它的正确方法. 硬件并行性和线程并行性是不同的.当您向网络子系统发出异步请求以从数据库中找到记录时,没有线程在那里等待结果.硬件实现了远远低于操作系统线程的并行性.

如果您想要更详细地解释硬件如何与操作系统协同工作以实现异步,请阅读Stephen Cleary的" 没有线程 ".

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

正如您可能想象的那样,编写控制流是非常困难的,您可以放弃现在正在做的事情,去做其他事情,并无缝地从中断的地方继续.这就是我们让编译器完成这项工作的原因!"等待"的重点在于,它允许您通过将这些异步工作流描述为同步工作流来管理这些异步工作流.在任何地方,你都可以把这个任务放在一边,稍后回到它,写下"等待".编译器将负责将您的代码转换为许多小块,每个小块都可以在异步工作流中进行调度.

更新:

在你的上一个例子中,它们之间会有什么区别


eggtask = FryEggAsync(); 
egg = await eggtask; 
toasttask = MakeToastAsync(); 
toast = await toasttask; 
Run Code Online (Sandbox Code Playgroud)
egg = await FryEggAsync(); 
toast = await MakeToastAsync();?
Run Code Online (Sandbox Code Playgroud)

我假设它同步调用它们但是异步执行它们?我不得不承认我以前从未单独等待任务.

没有区别.

FryEggAsync被调用时,它被称为无论await之前或不出现. await是一个运营商.它操作从调用返回的东西FryEggAsync.它就像任何其他运营商一样.

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

让我再说一遍:await你放在一个呼叫网站上并不是魔法粉尘,而且这个呼叫网站突然转移到了另一个线程.该调用发生时调用发生时,调用返回一个,并且该值是一个对象,它是一个合法的操作数的参考await操作.

是的,

var x = Foo();
var y = await x;
Run Code Online (Sandbox Code Playgroud)

var y = await Foo();
Run Code Online (Sandbox Code Playgroud)

是一样的,同样的

var x = Foo();
var y = 1 + x;
Run Code Online (Sandbox Code Playgroud)

var y = 1 + Foo();
Run Code Online (Sandbox Code Playgroud)

是一回事.

所以让我们再来一次,因为你似乎相信await 导致异步的神话.它不是.

async Task M() { 
   var eggtask = FryEggAsync(); 
Run Code Online (Sandbox Code Playgroud)

假设M()被调用.FryEggAsync叫做.同步.没有异步调用这样的东西; 你看到一个电话,控制传递给被叫方,直到被叫方返回.被调用者返回一个任务,该任务代表将来可用的鸡蛋.

怎么FryEggAsync做?我不知道,我也不在乎.我所知道的就是我称之为,并且我得到了一个代表未来价值的对象.也许该值是在不同的线程上生成的.也许它是在这个线程上生成的,但将来会产生.也许它是由专用硬件产生的,如磁盘控制器或网卡.我不在乎.我关心我找回一项任务.

  egg = await eggtask; 
Run Code Online (Sandbox Code Playgroud)

现在我们接受这个任务并await问"你做完了吗?" 如果答案是肯定的,则egg给出任务产生的值.如果答案为否则M()返回Task表示"M的工作将在未来完成".M()的剩余部分被注册为继续eggtask,因此当eggtask完成时,它将M()再次调用并且不是从头开始,而是从赋值到它egg.M()在任何点方法都是可恢复的.编译器为实现这一点做了必要的魔术.

所以现在我们回来了.线程继续做它做的任何事情.在某些时候,鸡蛋已准备就绪,因此eggtask会调用继续,这会导致M()再次调用.它从它停止的地方恢复:将刚生产的鸡蛋分配给egg.现在我们继续卡车运输:

toasttask = MakeToastAsync(); 
Run Code Online (Sandbox Code Playgroud)

同样,调用返回一个任务,我们:

toast = await toasttask; 
Run Code Online (Sandbox Code Playgroud)

检查任务是否完成.如果是,我们分配toast.如果没有,那么我们的M返回()再次,和延续toasttask是*的M的余数().

等等.

消除task变量没有任何密切关系.分配值的存储; 它只是没有给出一个名字.

另一个更新:

是否有一个案例可以尽早调用任务返回方法但是尽可能晚地等待它们?

给出的例子是这样的:

var task = FooAsync();
DoSomethingElse();
var foo = await task;
...
Run Code Online (Sandbox Code Playgroud)

一些情况需要做.但是我们在这里退后一步.await运算符的目的是使用同步工作流的编码约定来构造异步工作流.所以要考虑的是那个工作流程什么?一个工作流程强加于一组相关的任务的顺序.

查看工作流程中所需排序的最简单方法是检查数据依赖性.你不能在烤面包机出来之前制作三明治,所以你必须在某个地方获得吐司.由于AWAIT提取从完成任务的价值,我们有了是的await 某处创建烤面包机任务和三明治的创造之间.

您还可以表示对副作用的依赖性.例如,用户按下按钮,因此您想播放警笛声,然后等待三秒钟,然后打开门,然后等待三秒钟,然后关闭门:

DisableButton();
PlaySiren();
await Task.Delay(3000);
OpenDoor();
await Task.Delay(3000);
CloseDoor();
EnableButton();
Run Code Online (Sandbox Code Playgroud)

完全没有任何意义可言

DisableButton();
PlaySiren();
var delay1 = Task.Delay(3000);
OpenDoor();
var delay2 = Task.Delay(3000);
CloseDoor();
EnableButton();
await delay1;
await delay2;
Run Code Online (Sandbox Code Playgroud)

因为这不是理想的工作流程.

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

  • @EricLippert这篇文章让我对所有的Await/Async都非常清楚,感谢您花时间撰写一篇巨大的综合帖子! (7认同)