async/await:让异步看起来像同步
殊途同归
终于到了这一章的终极武器——async/await。
还记得餐厅的故事吗?
- 回调:服务员口头通知你,容易搞混
- Promise:给你取餐号,但你得一直盯着号码牌看
- async/await:你坐在座位上,菜来了服务员直接端上来,你什么都不用想
用了 async/await,异步代码写起来就像同步代码一样自然——该等就等,但程序不会卡死。
最有意思的是:Python 和 JavaScript 在这里使用了完全相同的关键字:async 和 await。它们各自走了不同的路,最终殊途同归。
基本语法
两边的结构几乎一模一样:
| Python | JavaScript | |
|---|---|---|
| 声明异步函数 | async def func(): | async function func() {} |
| 等待异步操作 | await something | await something |
| 模拟等待 | await asyncio.sleep(1) | await new Promise(r => setTimeout(r, 1000)) |
| 启动入口 | asyncio.run(main()) | main()(顶层可以直接调用) |
没错,
async和await这两个关键字完全一样!这大概是 Python 和 JS 最像的地方了。
async/await 本质上是 Promise 的语法糖
在 JS 里,async 函数自动返回一个 Promise,而 await 就是等待一个 Promise 完成。换句话说,它只是让你用更舒服的方式写 Promise 代码:
用 async/await 改写"回调地狱"
还记得前两节那个"查用户 → 查订单 → 查商品"的例子吗?用 async/await 改写后,代码终于变得和 Python 一样清爽了:
三个版本的进化:
| 版本 | 代码风格 | 可读性 |
|---|---|---|
| 回调 | 层层嵌套 })})}) | 头疼 |
| Promise | .then().then().then() | 凑合 |
| async/await | 一行一行,像同步 | 舒服 |
错误处理:try/catch
在异步代码中处理错误也变得直觉了——直接用 try/catch(JS)或 try/except(Python):
对比 Promise 的 .catch(),try/catch 更像正常的同步错误处理——你写过同步的 try/catch,异步的也会了。
忘记写 await 是最常见的 async 错误!
async function main() {
const data = fetchData() // 忘了 await!
console.log(data) // 输出: Promise { <pending> }
}
不加 await,你拿到的是一个 Promise 对象,而不是它的结果。就像去取餐时只拿走了号码牌,却没有拿走菜。
Python 也一样——调用 async 函数不加 await 会得到一个协程对象,而且 Python 还会给你一个警告:RuntimeWarning: coroutine 'xxx' was never awaited。
并行执行:同时做多件事
await 一个接一个地等,是串行的。但有时候你想同时发多个请求——比如同时查询三个餐厅的菜单:
| Python | JavaScript | |
|---|---|---|
| 并行等所有 | asyncio.gather(task1, task2) | Promise.all([task1, task2]) |
| 参数形式 | 逐个传入 | 传入数组 |
事件循环:幕后大管家
你可能好奇:await 的时候程序在干什么?答案是——事件循环(Event Loop) 在幕后调度一切。
把事件循环想象成餐厅里的大堂经理:
- 他手里有一张待办清单(任务队列)
- 他依次处理清单上的事情
- 遇到要等的事(比如等厨房),他不会傻等,而是先去处理清单上的下一件事
- 厨房做好了会通知他,他再把"上菜"这件事加回清单
┌─────────────────────────────────────┐
│ 事件循环 │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ 任务1 │→ │ 任务2 │→ │ 任务3 │ │
│ └───────┘ └───────┘ └───────┘ │
│ ↑ │ │
│ └─────────────────────┘ │
│ 循环处理 │
└─────────────────────────────────────┘
↕ ↕
┌─────────┐ ┌─────────┐
│ 网络请求 │ │ 定时器 │
│ (等待中) │ │ (等待中) │
└─────────┘ └─────────┘
关键区别:
- JS 的事件循环是内置的、自动运行的。浏览器和 Node.js 天生就有事件循环——你写的每一行 JS 代码都在事件循环里。
- Python 的事件循环需要你手动启动:
asyncio.run(main())。Python 的默认模式是同步的,异步是"可选升级"。
问 AI:"并发(concurrency)和并行(parallelism)有什么区别?async/await 实现的是哪一种?"
这是一个经典面试题。简单来说:
- 并发:一个人同时处理多件事(不停切换注意力)
- 并行:多个人同时做不同的事
async/await 实现的是并发——只有一个线程,但它不会傻等。理解这个区别很重要。
一个接近真实的例子
让我们用 async/await 写一个更贴近实际的例子——模拟同时从多个 API 获取数据:
写一个"赛跑点餐"程序:
- 模拟 3 家外卖平台同时下单(每家配送时间随机 1-5 秒)
- 哪家先送到就先吃哪家(用
Promise.race或asyncio.wait的FIRST_COMPLETED) - 打印出是哪家平台最先送达,以及用了多少时间
Python 和 JS 各写一版。提示:JS 用 Promise.race,Python 用 asyncio.wait 配合 return_when=asyncio.FIRST_COMPLETED。
可以让 AI 帮你生成初始框架,然后自己修改完善。
完整对照表
| 概念 | Python | JavaScript |
|---|---|---|
| 声明异步函数 | async def func(): | async function func() {} |
| 等待异步结果 | await expr | await expr |
| 启动事件循环 | asyncio.run(main()) | 自动(浏览器/Node.js 内置) |
| 并行等待 | asyncio.gather(a, b, c) | Promise.all([a, b, c]) |
| 错误处理 | try/except | try/catch |
| 抛出错误 | raise ValueError(...) | throw new Error(...) |
| 暂停等待 | await asyncio.sleep(n) | await new Promise(r => setTimeout(r, n)) |
小结
一句话总结异步编程的进化史:
回调 → 能用,但嵌套太深(回调地狱)
Promise → 链式调用,拍平了嵌套(JS 专属)
async/await → 看起来像同步,写起来最舒服(两种语言殊途同归)
Python 和 JavaScript 在异步编程上最终选择了相同的语法。这说明 async/await 确实是目前最好的异步编程范式——简洁、直观、易于理解。
学完这一章,你已经掌握了现代编程中最重要的概念之一。下次看到网络请求卡住了、界面转圈圈了,你就知道——该请出 async/await 了。