代码之家  ›  专栏  ›  技术社区  ›  SledgeHammer

为什么现在web应用程序在等待/异步方面变得疯狂?[已关闭]

  •  46
  • SledgeHammer  · 技术社区  · 10 年前

    我来自后端/厚客户端背景,所以我可能遗漏了一些东西……但我最近查看了开源JWT令牌服务器的源代码,作者对await/async非常着迷。喜欢每一种方法和每一行。

    我知道模式是什么……在单独的线程中运行长时间运行的任务。在我的客户机繁忙的日子里,如果一个方法可能需要几秒钟的时间,我就会使用它,这样就不会阻塞GUI线程……但绝对不会在需要几毫秒的方法上使用它。

    这种对等待/异步的过度使用是web开发还是Angular之类的东西所需要的吗?这是在一个JWT令牌服务器中,所以甚至看不到它与其中任何一个有什么关系。这只是一个REST端点。

    如何使每一行异步都能提高性能?对我来说,旋转所有线程会降低性能,不是吗?

    3 回复  |  直到 10 年前
        1
  •  143
  •   chue x    10 年前

    我知道模式是什么……在单独的线程中运行长时间运行的任务。

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

    等待会发生 不 将操作放在新线程上。确保是这样 非常 对你来说很清楚。 等待将剩余的工作安排为高延迟操作的继续。

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

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

    如果你正在纳税,你发现你需要工作中的一个号码,而邮件还没有送达,你就没有 雇一个工人在邮箱旁等候 等候 .它是 异步等待结果 .

    这种对等待/异步的过度使用是web开发还是Angular之类的东西所需要的吗?

    这是为了管理延迟。

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

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

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

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

    与什么相比反应更灵敏?与不等待就调用这些方法相比?不,当然不是。与同步等待任务完成相比?当然,是的。

    不不不。停止思考并行性。不需要任何并行。

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

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

    三项任务。第三项任务取决于前两项的结果,但前两项任务并不相互依赖。因此,以下是一些工作流:

    • 鸡蛋做好后,在烤面包机里放一些吐司。盯着烤面包机。
    • 烤面包片做好后,把鸡蛋放在烤面包片上。

    问题是,你可能在煮鸡蛋的时候把吐司放在烤面包机里。替代工作流:

    • 把吐司放在烤面包机里。设置一个闹钟,当吐司结束时响起。
    • 检查您的邮件。交税。擦亮银器。无论你需要做什么。
    • 当两个警报都响起时,抓起鸡蛋和吐司,把它们放在一起,你就可以吃三明治了。

    你明白为什么异步工作流要高效得多吗?在等待高延迟操作完成时,您完成了大量工作。 但你没有雇佣鸡蛋厨师和烤面包厨师

    我建议的工作流程是:

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

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

    • 去做其他工作,直到闹钟响。
    • 去做其他工作,直到闹钟响。
    • 当警报响起时,组装三明治。

    此工作流效率较低 因为我们未能捕捉到吐司和鸡蛋任务具有高延迟和独立性的事实 。但它肯定比做更有效地利用资源 没有什么 当你在等鸡蛋煮的时候。

    这整件事的要点是:线程非常昂贵,所以 不要 启动新线程。相当地 在执行高延迟操作时,将线程投入工作,从而更有效地利用线程 .等待 不

    也许计算是在另一个线程上进行的,也许它在磁盘上被阻塞了,不管怎样。没关系。关键是,等待是为了 管理 异步,不是 创建

    我很难理解在不使用并行的情况下异步编程是如何实现的。比如,如何让程序在等待鸡蛋的同时开始烤面包,而不同时运行DoEggs(),至少在内部运行?

    回到类比。你正在做鸡蛋三明治,鸡蛋和吐司正在烹饪,所以你开始阅读邮件。鸡蛋煮好后,你把邮件放在一边,把鸡蛋从火上拿开。然后你回到邮件。然后烤面包片做好了,你做三明治。三明治做好后,你读完邮件。 你是如何做到这一切的,没有雇佣员工,一个人读邮件,一个人煮鸡蛋,一个人烤面包,一个人做三明治? 你只用一个工人就完成了这一切。

    你是怎么做到的?通过将任务分解为小部分,记下哪些部分必须按哪个顺序完成,然后 协同多任务处理 这些碎片。

    如今,拥有大平面虚拟内存模型和多线程进程的孩子们认为这是一直以来的事情,但我的记忆可以追溯到Windows3时代,那时的Windows3没有这一切。如果你想两件事情“并行”发生,你就这样做了:把任务分成小部分,然后轮流执行。整个操作系统都基于这个概念。

    现在,你可以看看这个类比,然后说“好吧,但有些工作,比如烤面包,是由机器完成的”,并且 那个 是并行性的源泉。当然,我不需要雇佣工人来烤面包,但我在硬件方面实现了并行。这是正确的思考方式。 硬件并行和线程并行是不同的 。当您向网络子系统发出异步请求以从数据库中查找记录时 无线程

    如果您想更详细地解释硬件如何与操作系统协同工作以实现异步,请阅读“ There is no thread “斯蒂芬·克利里(Stephen Cleary)。

    所以当你看到“异步”时,不要认为“并行”。如果存在以下情况,请考虑“高延迟操作拆分为小块” 许多的 如果这些操作的各个部分不相互依赖,则可以 协同交织 在一个线程上执行这些片段。

    正如你所想象的那样 非常困难 为了编写控制流,你可以放弃你现在正在做的事情,去做其他事情,然后无缝地从你停止的地方开始。这就是为什么我们让编译器这样做!“等待”的意义在于,它可以通过将异步工作流描述为同步工作流来管理这些工作流。在任何地方,你都可以把这个任务放在一边,然后再回来,写上“等待”。编译器将负责将代码转换成许多小片段,每个小片段可以在异步工作流中调度。

    更新:

    在上一个示例中


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

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

    我假设它同步调用它们,但异步执行它们?我必须承认,我以前从未想过要单独等待这项任务。

    没有区别。

    什么时候 FryEggAsync 打电话 无论是否 await 是否出现在它之前。 等候 是一个 操作人员 .它对物体起作用 返回 从呼叫到 异步煎蛋 它和其他运营商一样。

    让我再说一遍: 等候 是一个 操作人员 它的操作数是一个任务。可以肯定,这是一个非常不寻常的运算符,但从语法上来说,它是一个运算符,它对 价值 就像其他运营商一样。

    等候 并不是你放在一个调用站点上,突然那个调用站点被远程到另一个线程。调用发生时,调用返回一个 价值 ,该值为 对对象的引用,该对象是 等候 操作人员 .

    所以是的,

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

    和

    var y = await Foo();
    

    都是一样的,都是一样

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

    和

    var y = 1 + Foo();
    

    都是一样的。

    等候 原因 异步。但事实并非如此。

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

    认为 M() 被调用。 异步煎蛋 被调用。同步。没有异步调用这回事;您看到一个调用,控制权传递给被调用方,直到被调用方返回。被调用者返回 代表未来可用鸡蛋的任务 .

    异步煎蛋 这样做吗?我不知道,我也不在乎。我只知道我调用它,然后我得到一个表示未来值的对象。也许这个值是在不同的线程上生成的。也许它是在 这个线程 但是 未来 可能是由专用硬件生产的,比如磁盘控制器或网卡。我不在乎。我在乎我能得到一个任务。

      egg = await eggtask; 
    

    现在我们接受这个任务 等候 问它“你做完了吗?”如果答案是肯定的,那么 egg 被赋予任务所产生的价值。如果答案是否定的,那么 M() Task 代表“M的工作将在未来完成”。M()的其余部分注册为 eggtask ,所以什么时候 鸡蛋任务 完成,它将调用 M() 再把它捡起来 不是从一开始 ,但来自 分配给 鸡蛋 可在任何时候恢复 方法编译器会使用必要的魔法来实现这一点。

    现在我们回来了。线程继续执行它所做的任何操作。在某个时刻,鸡蛋已经准备好了,所以 鸡蛋任务 被调用,这将导致 M() 再次被呼叫。它在停止时恢复:将刚生产的鸡蛋分配给 鸡蛋 现在我们继续卡车运输:

    toasttask = MakeToastAsync(); 
    

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

    toast = await toasttask; 
    

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

    等等

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

    另一个更新:

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

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

    有 一些 这是一个很好的例子。但让我们回到这里。目的 等候 使用同步工作流的编码约定构造异步工作流 所以要考虑的是 那是什么工作流程 ? A. 工作流 对一组相关任务进行排序。

    查看工作流中所需顺序的最简单方法是检查 数据相关性 。由于await从已完成的任务中提取值,因此必须有一个await 在某处 在创建烤面包机任务和创建三明治之间。

    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;
    

    因为这不是所需的工作流。

    因此,对您的问题的实际回答是:将等待推迟到实际需要价值的时候是一种很好的做法,因为这增加了有效安排工作的机会。但你可以走得太远;确保实现的工作流是您想要的工作流。

        2
  •  3
  •   Kirlac    10 年前

    一般来说,这是因为一旦异步函数能更好地与其他异步函数配合使用,就开始失去异步的好处。因此,调用异步函数的函数最终都是异步的,它会在整个应用程序中传播,例如,如果您与数据存储进行异步交互,那么使用该功能的东西也会成为异步的。

    当您将同步代码转换为异步代码时,您会发现,如果异步代码一直向下(或向上,如果您愿意)调用并由其他异步代码调用,则效果最好。其他人也注意到异步编程的传播行为,称之为传染性,或将其比作僵尸病毒。无论是乌龟还是僵尸,异步代码都会使周围的代码变得异步。这种行为在所有类型的异步编程中都是固有的,而不仅仅是新的async/await关键字。

    资料来源: Async/Await - Best Practices in Asynchronous Programming

        3
  •  1
  •   bazza    10 年前

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

    我的观点是,异步/等待只是修饰软件系统的一种方式,以避免不得不承认,实际上,许多系统(尤其是那些具有大量网络通信的系统)最好被视为参与者模型(或者更好的是,通信顺序过程)系统。

    有了这两个因素,整个要点就是你要等待几件事情中的一件完成,当一件完成时采取必要的行动,然后再回到等待中。具体来说,您正在等待来自其他地方的消息,阅读它,并根据内容采取行动。在*nix中,等待通常通过调用epoll()或select()来完成。

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

    然而,一旦你克服了不再调用方法而是简单地来回传递消息的想法,一切就变得很自然了。这在很大程度上是一件“请做这件事”,“当然,答案在这里”的事情,许多这样的互动相互交织。在循环的顶部用一个大的WaitForLotsOfThings()调用来结束它,仅仅是一个明确的确认,即您的程序将等待,直到它有事情要做,以响应与它通信的许多其他程序。

    Windows如何使其变得困难

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

    对于前者,表示“停止收听另一个参与者”的消息(或者实际上是超时)很容易处理,您只需在下次等待时将该参与者从您要收听的列表中排除即可。

    我在一定程度上吹毛求疵。Proactor在具有动态连接的系统中非常有用,Actor会掉入系统,然后再次掉出。如果你有固定数量的演员,并且有永远不会消失的通信链接,那么反应器就可以了。然而,考虑到反应器系统很容易在前置器平台上实现,但前置器系统不能在反应器平台上轻松实现(时间不会倒流),我觉得Window的方法特别令人恼火。

    因此,不管怎样,异步/等待肯定仍在反应堆中。

    撞击后爆震

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

    C++的Boost asio也是reactor,即使在*nix上也是如此,很大程度上似乎是因为他们希望有一个Windows实现。

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

    对于Windows上的cygwin系列POSIX运行时,它们必须通过每个文件描述符有一个线程来实现select()、epoll()等 投票 (是的, 投票 !!!!) 用于输入数据的底层套接字/串行端口/管道,以便重新创建POSIX的例程。Yeurk!是的!cygwin-dev邮件列表上的评论可以追溯到他们实现该部分的时候,这让人读起来很有趣。

    演员不一定慢

    值得注意的是,短语“传递消息”并不一定意味着到处传递副本——有很多参与者模型的公式,你只是在传递消息引用的所有权(例如,数据流,C#中任务并行库的一部分)。这使它更快。我还没有抽出时间来研究Dataflow库,但它并没有真正让Windows proactor突然出现。它并没有为您提供一个处理各种数据载体(如套接字、管道、队列等)的actor模型proactor系统。

    所以,刚刚抨击了Windows和它的劣质反应堆架构,一个有趣的地方是Windows 10现在运行Linux二进制文件。我非常想知道,微软是如何实现select()、epoll()下面的系统调用的,因为它必须在套接字、串口、管道和POSIX(文件描述符)土地上的其他所有东西上运行,而Windows上的其他东西都不能运行?我真想知道那个问题的答案。