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 函数改成异步: ```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
还是Go语言的Goroutine用起来舒服~
登录