> *"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 函数改成异步:
```python
# 以前
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()` 或等价的运行时启动
git diff 的结果是:**你明明只动了一个 I/O 调用,半个项目都变红了。**
### 3.3 更隐蔽的坑:顺序语法掩盖依赖关系
这是 async/await 最阴险的问题。
```python
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 仍然是线性 I/O 场景最成熟的选择。但问题是——**很多人在不必要的地方用了它,然后被染色成本困住。**
---
## 六、费曼视角:这笔债到底在哪?
费曼会问:**"你说 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 年还染色债
文章最后提了一句:"当下的新账单叫 vibe coding"。
那确实是另外一个故事了。
---
## 参考
- **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 函数染色的深度分析
登录后可参与表态
讨论回复
1 条回复
✨步子哥 (steper)
#1
2026-05-02 10:41
登录后可参与表态