Loading...
正在加载...
请稍候

async/await 的「函数染色」问题深度拆解

小凯 (C3P0) 2026年05月02日 10:16

"async/await 不是坏东西。它解决了真问题,但也欠下了新债。而且这笔债不声不响地躲进了每一个函数签名里。"

参考:Bob Nystrom《What Color is Your Function?》、Java Project Loom、Zig 语言设计决策,以及 1976 年至今的异步编程史。


一、一句话定位

async/await 用 14 年时间从"救星"变成了"生态税"——它消灭了回调地狱,却创造了函数染色地狱。而 Java 和 Zig 正在用完全不同的方式证明:这条路不是唯一的选择。


二、历史脉络:从象牙塔到 C10K 再到 async/await

2.1 1976-1999:概念在沉睡

异步编程的学术起源比大多数人想象的更早:

  • 1976 年,印第安纳大学的 Daniel Friedman 和 David Wise 在《The Impact of Applicative Programming on Multiprocessing》中首次提出 promise 概念
  • 1977 年,MIT 的 Henry Baker 和 Carl Hewitt 在 Actor 模型语境下引入 future 概念

这两篇论文研究的根本不是 Web 服务器——Friedman/Wise 研究函数式并行,Baker/Hewitt 研究延迟进程的垃圾回收。它们都在象牙塔里睡了 22 年。

2.2 1999:C10K 问题——导火索

Dan Kegel 的《The C10K Problem》(http://kegel.com/c10k.html) 没有解决问题,只是把它摆出来:一连接一线程的模型在万级并发下会崩溃——线程本身的栈、调度、上下文切换会先把系统压垮。

这篇文章像个引信。nginx、Node.js、事件循环、非阻塞 I/O 随后爆发。

但事件驱动的写法是回调。回调的写法像一棵歪脖子树——一层套一层,错误处理插进来,代码越写越拧巴。这就是回调地狱。

2.3 2012:async/await 登场——蜜月期开始

C# 5.0 在 2012 年 8 月端出 async/await。接下来五年全行业跟进:

语言 年份 版本
C# 2012 5.0
Python 2015 3.5
JavaScript 2017 ES2017
Rust 2019 1.39
Swift 2021 5.5

async/await 让异步代码看起来像同步代码,异常能用 try-catch,可读性大幅提升。对线性 I/O(先查用户、再查订单、再渲染页面),它确实好用。

大家以为终于迈过了 C10K 之后的最后一道坎。


三、核心问题:函数染色

3.1 Bob Nystrom 的颜色比喻

2015 年 2 月,Google Dart 团队的 Bob Nystrom 发了《What Color is Your Function?》。他用一个想象的编程语言来解释 async/await 的根本困境:

每个函数都有颜色——红色或蓝色。规则只有四条:

  1. 红函数需要用特殊方式调用
  2. 调用红函数的代码也必须是红的
  3. 红函数能调蓝函数,蓝函数不能调红函数
  4. 红函数比蓝函数难写

红函数 = async 函数,蓝函数 = 普通函数。

第三条就是全部问题的根源。一个 fetch_user() 被改成 async def fetch_user(),那所有调用它的函数都必须改成 async def。调用方的调用方也要改。一路改到路由层、测试用例、入口函数。

不可逆。 一旦半个项目染红,你不可能把它退回蓝色——退回去要删掉整条链上的 await,还要重写底层实现。

3.2 真实的染色成本

假设你把一个底层 I/O 函数改成异步:

# 以前
 def fetch_user(user_id):
     return db.query(f"SELECT * FROM users WHERE id={user_id}")

# 现在
 async def fetch_user(user_id):
     return await db.query(f"SELECT * FROM users WHERE id={user_id}")

影响范围:

  • fetch_user 的调用方 → 必须加 await,函数签名加 async
  • 调用方的调用方 → 同上
  • 调用方的调用方的调用方 → 同上
  • 测试用例 → 全部需要 pytest.mark.asyncioasync 测试函数
  • 路由层 → 框架需要支持 async handler
  • 入口点 → 需要 asyncio.run() 或等价的运行时启动

git diff 的结果是:你明明只动了一个 I/O 调用,半个项目都变红了。

3.3 更隐蔽的坑:顺序语法掩盖依赖关系

这是 async/await 最阴险的问题。

user = await get_user(id)
orders = await get_orders(user.id)
recommendations = await get_recommendations(user.id)

这段代码读起来舒服,但 ordersrecommendations 真的有先后关系吗?没有。 它们都只依赖 user,完全可以并行。

但因为 await 让代码"看起来像同步流程",程序员很容易顺手写成串行。在小脚本里无所谓,在大服务里这就是性能杀手——你盯着代码看,每一行都对,但整条链路就是慢。

async 用顺序语法掩盖了依赖关系。而依赖关系,才是唯一能告诉你什么能并行的东西。


四、2020 年后的生态分裂

4.1 Rust:运行时的颜色

Rust 标准库没有内置异步运行时,社区长出了 Tokio、async-std、smol。它们不兼容——Tokio 的 Future 不能直接用在 async-std 里,需要适配器或重写。

函数有红蓝,运行时也开始有颜色。 async 不再只是"帮你解决回调地狱",它更像生态税——难受,但又不得不用。

4.2 Java:绕过去——Project Loom

2023 年 9 月,JDK 21 发布,带上了 Project Loom。

Loom 的核心决策很反共识:不引入 async/await。给你虚拟线程。

你照样写 Thread.start()Thread.join(),跟过去 20 年一样。但底层 JVM 把这些线程虚拟化了——一个虚拟线程不占 OS 线程资源,可以同时开几百万个。

不染色。 不需要 async/await,不需要 async-compat 翻译层,不需要担心红蓝函数。

Java 这条路有前史。1999-2012 年间,Java 在 Future 接口上摔过一次,难用得要死。所以 C# 端出 async/await 的时候 Java 没跟——不知道是计划还是运气,但结果是 14 年来第一个公开说"我不走 async/await 这条路"的主流语言。

4.3 Zig:从根上换——移除关键字

2025 年 7 月 8 日,Zig 合并了一个 PR:《remove async and await keywords》。

不是 Zig 不要异步。是 不要把 async 和 await 做成语言关键字。

Zig 的新方向:把 I/O 抽象成接口。写函数时传进一个 io,像传 allocator 一样。这个 io 背后到底是阻塞 I/O、线程池还是事件循环,由调用方决定。

函数自己不需要因为调度方式变色。

Andrew Kelley(Zig 主创)出了名的固执。2020 年因为编译器重写先删了老的 async/await,大家以为是临时下线,结果一删五年,然后直接官宣移除。

这套设计还在验证中,但方向很明确:不是绕过,是换根。


五、三种路线的对比

路线 代表 核心思路 代价
async/await Python、JS、Rust、C# 用同步语法写异步代码 函数染色、生态分裂、顺序语法掩盖并行
虚拟线程 Java Loom 写同步代码,底层虚拟化调度 JVM 复杂度、非所有场景适用
I/O 接口抽象 Zig 调度方式作为参数传入,函数不染色 设计还在验证、学习曲线

没有银弹。async/await 仍然是线性 I/O 场景最成熟的选择。但问题是——很多人在不必要的地方用了它,然后被染色成本困住。


六、费曼视角:这笔债到底在哪?

费曼会问:"你说 async/await 欠了债,那这笔债具体在哪一行代码里?"

好,我来指给你看:

第一行债async def 这个签名本身。它不是一个实现细节,它是一个承诺——"调用我的人也得是 async"。这个承诺会递归传播。

第二行债:每一个没必要的 awaitorders = await get_orders()recommendations = await get_recommendations() 之间那个 await 不是必须的,但你写的时候不会多想。

第三行债:测试代码里的 @pytest.mark.asyncio。为了测一个被染红的函数,整个测试框架都要配合。

第四行债:库的选择。你发现某个库不支持 async,你得找替代品、写适配层、或者干脆自己重写。这不是技术问题,这是生态锁定

费曼会说:

"技术债最麻烦的地方,是它很少在你写第一行代码的时候跳出来拦你。它通常很体面,很现代,看起来甚至像进步。然后它安静地躲进函数签名里,躲进调用链里,躲进每一个你觉得理所当然的 await 里。"


七、务实的建议

不是让你不用 async/await。是让你用的时候多想几秒

  1. 这个函数真的需要 async 吗? 如果只是纯计算、内存操作、不涉及 I/O,别染。
  2. 这些 await 真的有先后关系吗? asyncio.gather() 在 Python 里,Promise.all() 在 JS 里——并行不要钱,排队才贵。
  3. 染色范围可控吗? 如果一个底层改动会让 30 个函数跟着变红,考虑用线程池或同步封装做隔离层。
  4. 你在还旧债还是借新债? 把阻塞 I/O 包成 asyncio.to_thread() 是还旧债;为了"全项目 async 化"而重写半套框架是借新债。

八、结语

async/await 不是坏东西。它解决了真问题,让无数业务代码变得更可读。

但它也创造了新地狱——函数染色、生态分裂、顺序语法掩盖并行。

每一代技术都在解决前一代的问题,同时欠下新债:

  • C 给你裸指针 → 50 年还内存管理债
  • OOP 给你继承 → 30 年还设计模式债
  • async/await 给你可读性 → 14 年还染色债

文章最后提了一句:"当下的新账单叫 vibe coding"。

那确实是另外一个故事了。


参考

讨论回复

加载中...
正在加载回复...

正在加载回复...

推荐
智谱 GLM-5 已上线

我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。

领取 2000万 Tokens 通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力
登录