public:lang:async

Async 异步编程

  • What Async Promised and What it Delivered 异步编程的承诺与实际成果
  • 操作系统线程开销不菲:在当代 Linux 系统中,每个线程都会为其栈保留虚拟地址空间,创建一个线程需要消耗数十微秒的时间。上下文切换发生在内核空间,会燃烧 CPU 周期,而 O(n) 就绪状态轮询(如 select、poll)在大规模场景下会不断叠加开销。当一台服务器需要处理数千个并发连接时,若采用每连接一线程的架构,意味着数千个线程都将占用内存并竞争调度资源。系统将大量时间耗费在线程管理上,而这些资源本可用于执行更有价值的实际工作。这就是 C10K 问题,由丹·凯格尔于1999年提出。如果你正在构建一个网络服务器、聊天系统或任何需要处理大量并发连接的系统,你就需要一种方式来实现并发处理,而无需为每个连接分配一个线程
  • 解决方式一:回调函数 Callbacks:
    • 与其等待I/O操作完成,不如注册一个在操作结束时调用的函数,然后继续处理下一个任务。比如 elect、poll、epoll、kqueue, 比如 Node.js,Nginx 。
    • 缺点:回调颠倒了控制流;callback hell 回调地狱
    • 缺点:破坏了错误处理的完整性。每个回调函数都需要独立的错误处理路径。由于不存在调用栈(回调函数在其注册上下文之外运行),错误无法自然地沿调用链向上传播。在回调链中处理部分失败的情况,意味着需要在链条中的每个函数中传递错误状态
    • 缺点:回调函数无法实现取消操作。当启动一个异步操作后,若决定不再需要其结果,通常没有办法将其终止。回调最终仍会被触发,而你的代码需要处理“已不再关心结果”这一情形
  • 解决方式二:Promises and Futures
    • 这是一种承诺(JavaScript)或未来值(Java、Rust等)。这一概念可追溯到1977年Baker和Hewitt的研究,但直到2010年代的C10K压力才将其推入主流编程。JavaScript在ES2015中按照社区驱动的Promises/A+规范标准化了原生Promise,而Java 8则引入了CompletableFuture
    • 优点:Promise 具有可组合性:promise.then(f).then(g) 以管道形式呈现,而非嵌套的金字塔结构
    • 优点:错误处理也更集中:在链式调用的末尾添加一个 `.catch()` 即可捕获任意步骤的失败
    • 缺点:Promise(承诺)是一次性的。一个 Promise 只会被决议一次。这使得它们不适合用于建模流、事件、重复消息或任何持续性的通信。例如,接收一系列消息的 WebSocket 并不符合“一个将在未来存在的值”这一模式
    • 缺点:组合方式相对笨拙
    • 缺点:错误静默消失。JavaScript 中的 Promise 若未通过 .catch() 处理拒绝状态,最初只会直接吞没错误。丢失的值导致故障无法被察觉。这种问题严重到 Node.js 最终将未处理的拒绝从警告升级为进程崩溃,而浏览器则新增了 `unhandledrejection` 事件。本为改进错误处理而设计的特性,反而制造出一类回调函数中从未存在过的全新静默失败。
    • 缺点:类型分裂。每个函数现在可能返回一个值,也有可能返回一个 Promise。因此,调用者需要知道是哪一种,而库也需要决定提供哪一种。
  • 解决方式三:Async/Await
    • async/await机制(最初由C#在2012年开创,随后被JavaScript(ES2017)、Python(3.5)、Rust(1.39)、Kotlin、Swift和Dart采用。业内迅速采纳了这一模式,JavaScript框架全面支持,Python的asyncio成为并发I/O的标准方式,而Rust则稳定了async/await作为高性能网络编程的路径。短短几年间,async/await便成为多数主流语言中编写并发I/O代码的默认方式
    • 优点:让异步代码看起来像顺序执行
    • Paying the Function Coloring Tax:鲍勃·奈斯特罗姆发表了《你的函数是什么颜色?》一文。这篇文章设想了一门语言,其中每个函数要么是“红色”,要么是“蓝色”。红色函数可以调用蓝色函数,但蓝色函数要调用红色函数则必须经过特殊仪式。每个函数都必须选择一种颜色,如果你从蓝色函数中调用了红色函数,那么该蓝色函数就必须变成红色——这种特性会像病毒般在整个代码库中蔓延
    • 这是对async/await机制的一个类比:异步函数是红色,同步函数是蓝色。异步函数调用同步函数毫无问题,但若要从同步函数中调用异步函数,则必须阻塞线程或重构代码。程序中每个函数都必须选择一种颜色,而这一选择会传播至每个调用者。
    • 缺点:函数类型分裂:同步函数与异步函数,调用者需要依此区分设计模式与使用。一个同步思维设计模式下的项目,一旦启用了一个异步库,意味着一长串函数的修改。
    • 缺点:库的割裂:库和接口的提供者需要决定使用同步还是异步。在Python中,requests库(同步)和aiohttp(异步)是由不同开发者独立维护的两个项目,功能却完全相同。httpx最终实现了从同一个包中同时提供两种接口,然而这种改进之所以必要,正是由于同步与异步的人为割裂。
    • 缺点:新型 bug。O'Connor 记录了一种异步 Rust 的死锁类型,他称之为“futurelock”:一个 future 持有锁后停止轮询,而另一个 future 试图获取同一把锁。在线程模型中,持有锁的线程总会继续执行直到释放锁(除非你做了像 SuspendThread 这样人人皆知危险的操作)。而在异步 Rust 中,select!、缓冲流以及 FuturesUnordered 等标准工具会频繁停止轮询那些持有资源的 future。Oxide 公司最初遇到的 futurelock 问题,需要通过核心转储和反汇编器才能诊断出来
    • 缺点:顺序陷阱:与 Promise 相比,Promise 要求你分析业务逻辑的依赖、并发与串行再组合代码,而 async 容易掉入串行认知陷阱:把两个独立可并发的异步动作写成了串行,损失了性能。
  • Go 语言刻意采用了 goroutine,通过接受更重的运行时负担,彻底避免了函数着色问题。(Go 实际上通过 context.Context 引入了一种着色形式,该上下文通过调用传播以实现取消操作)
  • Java 的 Project Loom(Java 21 中的虚拟线程)也做出了不同的选择:轻量级线程在外观和行为上如同常规线程,因此无需任何代码改变颜色。Loom 团队明确将函数着色视为他们希望规避的问题。
  • Zig 更进一步:它完全移除了编译器层面的 async/await,转而围绕 I/O 操作所接受的 I/O 接口参数进行重构。运行时(线程化、事件循环,或用户提供的任何机制)负责实现该接口。函数签名不会因其调度方式而改变,async/await 也从语言关键字降级为库函数。不过也有观点认为,I/O 参数本身已构成一种“着色”形式。
  • public/lang/async.txt
  • 最后更改: 2026/05/07 15:08
  • oakfire