返回

文章详情

东京/雷冉陷阱及异步/等待为何无法实现并发

Hacker News2026年7月16日 02:06

在过去的十年中,异步/等待赢得了并发战,因为它异常简单。它允许开发者编写看起来几乎与同步代码相同的异步代码。但在这种熟悉的语法背后,隐藏着巨大的结构复杂性。它掩藏了控制流,模糊了硬件现实,最终将调度的负担推回给开发者。Rich Hickey 在他的演讲《简单的简单》中完美地阐述了这一点:“简单”是指熟悉且触手可及的事物,而“简单的”则是指结构上不纠缠的事物。异步/等待易于编写,但操作起来却极其复杂。Rob Pike 在他2023年GopherConAU的演讲中谈到了这种架构转变:与 goroutines、channels 和 select 相比,对于语言实现者来说,异步/等待更易于构建……但它将一些复杂性推回给程序员,常常导致 Bob Nystrom 所称的“有色函数”。 […] 重要的是,无论你提供什么并发模型,你只能提供一次,因为提供多种并发实现的环境可能会出现问题。Pike 关于“多种并发实现”和异步/等待的评论正是今天在生产中失败的原因。 生产陷阱:将异步与并发混淆 异步/等待的根本陷阱在于它将异步(在等待I/O时让出控制权)与并发(同时处理多个任务)混为一谈。语法就是一个陷阱,因为它把交错的状态机伪装成独立的、顺序的线程。被这个幻觉迷惑,开发者按照阻塞代码的方式编写异步函数——通过网络获取数据库记录,然后立即处理数据。但是,当数据处理涉及解析10MB的JSON负载、遍历一个巨大的集合或执行计算密集的加密证明时,会发生什么呢?合作执行器停止了。 期望:快速 I/O 任务快速让出控制权。高吞吐量。传入网络流量 任务队列(稳定/低) 执行器(1个操作系统线程) I/O 让出 I/O 让出 现实:计算陷阱 一个CPU任务阻塞执行器。队列爆炸。传入网络流量 任务队列(无限制) OOM 崩溃 执行器(1个操作系统线程) 重计算任务(bcrypt,解析等) 线程暂停 在像Rust的Tokio或Node.js这样的合作运行时中,线程在遇到await点之前不会让出控制权。在一个函数中的50毫秒CPU密集型任务会阻塞整个执行线程。突然之间,成千上万的无关网络请求延迟激增,系统变得无响应。与此同时,硬件几乎没有被充分利用。 破碎的承诺:人类在环调度器 当这些延迟激增时,答案总是相同的:分离你的运行时。对I/O使用Tokio,将CPU密集型工作发送到像Rayon这样的专用线程池。最近的事后分析突显了由此引发的灾难。PostHog和Meilisearch的工程团队记录了在生产中解开这些复杂性的痛苦现实。开发者必须仔细分析每个函数以决定它属于“I/O池”还是“计算池”,然后手动协调它们之间的消息传递边界。如果开发者必须手动划分I/O和计算,严格监管边界以防止死锁,并在两个有不同心理模型的不同运行时之间转移数据,那么异步抽象就失败了。 语言功能承诺隐藏并发的复杂性。相反,它把应用程序开发者变成了人类在环调度器。 默认情况是无限制的就是OOM 默认 异步/等待运行时的第二种失败模式是在无边界容量方面的高效性。调用 tokio::spawn(...) 的成本很低。当下游数据库在流量高峰期间变慢时,入口网络循环愉快地继续接受连接并生成任务。由于异步任务和内存分配在这些生态系统中通常默认是无限制的,系统不会反击。正在进行的任务无限期排队。应用程序消耗RAM,直到操作系统的内存溢出(OOM)杀手猛烈终止进程。主要平台的事后分析一致揭示同样的根本原因:队列并不能解决超载问题,它们只是延迟崩溃,同时使问题变得灾难性。无限的容量是一个谎言,假装不然的默认设置是危险的。 工作窃取神话 当系统遇到这些瓶颈时,开发者通常要求更智能的、抢占式的工作窃取调度器来分配负载。假设是,如果一个核心闲置,它应该从一个繁忙的核心那里窃取任务以保证公平。但在大规模下,公平是吞吐量的敌人。工作窃取破坏了CPU缓存局部性。当WhatsApp在100多个核心的机器上将Erlang BEAM虚拟机推向极限时,系统陷入了僵局。正如Robin Morisset所详细描述的,试图窃取工作的空闲线程……

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡