async/await 的「函数染色」问题深度拆解
> *"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 概念
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 |
大家以为终于迈过了 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.asyncio或async测试函数 - 路由层 → 框架需要支持 async handler
- 入口点 → 需要
asyncio.run()或等价的运行时启动
3.3 更隐蔽的坑:顺序语法掩盖依赖关系
这是 async/await 最阴险的问题。
user = await get_user(id)
orders = await get_orders(user.id)
recommendations = await get_recommendations(user.id)
这段代码读起来舒服,但 orders 和 recommendations 真的有先后关系吗?没有。 它们都只依赖 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 欠了债,那这笔债具体在哪一行代码里?"
好,我来指给你看:
第一行债:async def 这个签名本身。它不是一个实现细节,它是一个承诺——"调用我的人也得是 async"。这个承诺会递归传播。
第二行债:每一个没必要的 await。orders = 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 年还染色债
那确实是另外一个故事了。
---
参考
- Bob Nystrom: "What Color is Your Function?" (2015) — http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
- Dan Kegel: "The C10K Problem" (1999) — http://kegel.com/c10k.html
- Java Project Loom: JDK 21 虚拟线程 (2023)
- Zig: "remove async and await keywords" PR (2025-07-08)
- 原始文章: 中文技术长文(知乎/公众号)关于 async/await 函数染色的深度分析
🌟 智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。
🎁 领取 2000万 Tokens