布朗大学最近发了一篇论文,叫《A Design Space Exploration of Async/Await》,把async/await这件事掰开了揉碎了讲。我看完之后的第一反应不是"学到了",而是"原来我以为自己懂的东西,其实只懂了一半都不到"。
做了很多年开发,从PHP到Node.js到React,异步编程几乎是我每天打交道的事。但我越来越觉得,大部分开发者对async/await的理解停留在"加了await就能拿到结果"这个层面。至于底下发生了什么,没多少人关心。直到出了问题才一脸懵:为什么我的请求卡住了?为什么并发数上不去?为什么CPU利用率这么低?
一、async/await不是并发模型,它只是糖
这是我首先想厘清的一件事。async/await解决的是"异步代码怎么写更好读"的问题,不是"程序怎么跑更快"的问题。它把回调地狱变成了看起来像同步的代码,但底下的并发模型完全没变。
JavaScript的async/await跑在单线程事件循环上。你await了一个Promise,代码看起来"停住了",但实际上只是把控制权还给了事件循环。如果这时候主线程上有个CPU密集型任务在跑,你的await就一直等不到结果。这不是bug,这是设计。
但Python的asyncio呢?也是单线程事件循环,但多了个东西叫TaskGroup,可以显式管理并发任务。Go更直接,goroutine是真正的轻量级线程,runtime帮你调度。Rust又不一样,它的Future是惰性的,你不poll它就不跑。
同样叫async/await,四种语言,四套完全不同的底层机制。如果你只写过JavaScript,很容易以为全世界的async都是事件循环加微任务队列。不是的。
二、后端出身的人,看异步的角度不一样
我最早写PHP,那时候PHP的并发模型非常简单——每个请求一个进程(或线程),处理完就销毁。根本不需要操心什么异步。后来写Node.js,一下子被扔进了事件循环的世界,花了很长时间才真正理解"非阻塞"到底意味着什么。
再后来做BFF层,用Node.js写中间服务,发现一个问题:很多前端转Node的同学,写出来的代码虽然是async/await,但本质上是"用同步思维写异步代码"。比如在一个循环里串行await多个请求,明明可以并发。比如不知道await后面那行代码可能在一个完全不同的事件循环tick里执行。
而后端出身的人写Node.js,往往会下意识地考虑几个问题:这个操作是CPU密集还是IO密集?连接池够不够?背压怎么处理?超时和重试的策略是什么?这些不是async/await能帮你解决的问题,但如果你不考虑,async/await反而会让你忽略它们。
我觉得这中间的认知差距,不在于语法本身,而在于你是否理解"并发"和"并行"的区别,是否理解"非阻塞"和"异步"的区别。这些概念很多人是混着用的,但它们是完全不同的东西。
三、布朗大学那篇论文讲了什么
简单说,他们把async/await的设计空间拆成了几个维度:
第一个维度是调度模型。谁来负责决定哪个异步任务在什么时候执行?JavaScript是事件循环,Go是goroutine scheduler,Rust是executor crate。调度模型决定了你的并发上限和调度开销。
第二个维度是取消机制。一个正在跑的异步任务,能不能取消?怎么取消?JavaScript的Promise没有原生的取消机制(AbortController是个补丁),Go的context.WithCancel是标准做法,Rust的Future drop了就算取消。取消这件事看起来简单,实际上涉及到资源清理、状态一致性等一系列问题。
第三个维度是错误传播。异步操作的错误怎么传递?JavaScript用try/catch包await,看起来和同步代码一样,但如果你的Promise链中间断了呢?如果reject了但没人catch呢?Go的err return和Rust的Result类型,在异步场景下的错误处理策略完全不同。
第四个维度是结构化并发。这是近几年才热起来的概念。简单说就是:异步任务的生命周期应该有明确的边界,父任务结束时,子任务应该被自动回收。JavaScript目前在这方面比较弱,Promise.all算是个雏形,但Promise.race和Promise.any的语义就容易让人踩坑。
四、对全栈开发者的实际影响
理论归理论,回到日常开发,我觉得有几个判断是实际有用的:
首先,不要无脑await。在循环里串行await是性能杀手。如果你的N个await之间没有依赖关系,用Promise.all并发执行。这一条看起来简单,但在代码审查中,十个PR里至少三个会犯。
其次,分清IO密集和CPU密集。在Node.js里,IO密集操作用async/await没问题,但CPU密集计算会阻塞事件循环。遇到这种情况,要么用Worker Threads,要么把计算挪到后端。见过不少人试图用async/await"解决"CPU密集的问题,那是方向性错误。
再次,注意背压。当你用async/await处理流式数据或批量请求时,如果生产速度远大于消费速度,内存会爆。async/await不会帮你做背压控制,这个得自己处理。在金融业务里,我们处理批量交易数据时,这个问题尤其突出。
最后,关注超时。Promise.race配合setTimeout是个常见的超时模式,但别忘了那个永远不会完成的Promise还在那跑着,占着资源。AbortController可以解决这个问题,但不是所有API都支持它。
五、异步编程的未来
看完布朗那篇论文,我最大的感触是:async/await可能只是一个中间态。
结构化并发(Structured Concurrency)在Python和Java社区已经开始落地了。Kotlin的coroutine scope、Java的Project Loom,都在尝试解决"异步任务的生命周期管理"这个根本问题。JavaScript社区也有人提过类似的提案,但目前还在早期。
另一个方向是effect systems,Kotlin的suspend、Scala的ZIO、Rust的async trait,都在尝试用类型系统来约束异步行为。这条路更学术,但长期来看可能更彻底。
对于做技术决策的人来说,这意味着:现在选型的异步框架,五年后可能需要重写。所以选的时候,不只看API好不好用,还得看看这个框架背后的并发模型有没有前瞻性。
小结
async/await用了这么多年,我到现在还在不断遇到新场景、新问题。它不是那种"学会语法就够了"的东西。调度模型、取消机制、错误传播、结构化并发——每一个维度都有深坑。
布朗那篇论文值得一读,不是因为它给了什么答案,而是它给了一个框架,让你重新审视那些你以为已经懂了的东西。
异步编程这件事,可能大部分人都还没等对。但至少,我们可以先搞清楚自己在等什么。”
添加新评论