0.7 异步与并发基础
概述
你已经非常熟悉 JS 的异步模型:单线程事件循环、Promise、async/await、Promise.all。Python 的 asyncio 提供了几乎同构的 async/await、Task、gather,底层也是单线程事件循环。最大的概念差异是 GIL(全局解释器锁)和 Python 多线程的"假并行"。这一章帮你把 Python 异步的心智模型接上。
JS/TS ↔ Python 对照表
核心概念
1. JS 事件循环 vs Python asyncio 事件循环
两者都是单线程 + 事件循环 + 协作式并发模型:
Node.js:事件循环由运行时自动管理,你写代码不需要显式启动它。
Python:事件循环需要显式启动,用
asyncio.run()作为入口。
// Node.js:事件循环自动跑,直接 await
async function main() {
await fetch("https://api.example.com");
}
main();
# Python:必须用 asyncio.run() 启动事件循环
import asyncio
async def main():
await some_async_call()
asyncio.run(main()) # 入口:启动事件循环,运行 main,再关闭
2. async/await 相同点与差异
语法高度相似,但有几个关键差异:
async function fetchData(url) {
const res = await fetch(url);
return res.json();
}
async def fetch_data(url):
res = await fetch(url) # 语法一致
return res.json()
差异点:
调用 async 函数不执行,返回 coroutine 对象:在 Python 中,
fetch_data(url)只是创建协程对象,必须 await 或用asyncio.run执行才会真正运行。JS 里调用 async 函数会立即开始执行并返回 Promise。
async def hi():
print("hi")
hi() # 只创建了 coroutine,什么都不打印!还会警告
asyncio.run(hi()) # 真正执行
没有顶层 await:Python 的
await只能出现在async def函数内,模块顶层不能await。
3. Promise vs Task / Future
JS 的 Promise 在 Python 里对应几个层次的概念:
// JS:async 调用立即返回 Promise 并开始执行
const p = fetchData(url); // 已开始执行
await p;
# Python:需要手动创建 task 才能"后台"执行
import asyncio
async def fetch_data(url):
...
task = asyncio.create_task(fetch_data(url)) # 调度执行,类似启动一个 Promise
result = await task # 等待结果
asyncio.create_task(coro)相当于"把一个协程调度到事件循环后台执行",类似 JS 里 Promise 的"立即执行"语义。await coroutine直接等待,是串行;create_task后统一await才是并发。
4. Promise.all vs asyncio.gather
// JS:并发请求多个
const [a, b, c] = await Promise.all([
fetch(url1),
fetch(url2),
fetch(url3),
]);
# Python:asyncio.gather 并发
a, b, c = await asyncio.gather(
fetch(url1),
fetch(url2),
fetch(url3),
)
gather接收多个 awaitable(协程或 task),并发执行并保持顺序返回结果。Promise.all里某个 reject 会立即整体 reject;gather默认也会传播异常,但可用return_exceptions=True收集而不中断。
results = await asyncio.gather(
fetch(url1), fetch(url2),
return_exceptions=True, # 类似 Promise.allSettled 的行为
)
5. asyncio.run 入口
asyncio.run(main()) 是 Python 异步程序的唯一入口,它做三件事:创建事件循环、运行 main、关闭循环。
// JS:直接写即可
(async () => {
await doWork();
})();
# Python
import asyncio
async def main():
await do_work()
if __name__ == "__main__":
asyncio.run(main())
6. 阻塞 vs 非阻塞
和 JS 一样,事件循环里绝不能放阻塞调用,否则整个循环卡死:
// JS:错误示范,阻塞事件循环
const fs = require("fs");
fs.readFileSync("/big/file"); // 同步阻塞,卡住循环
# Python:错误示范,阻塞事件循环
import time
import asyncio
async def main():
time.sleep(5) # 同步 sleep,阻塞整个 loop!应该用 await asyncio.sleep(5)
用
await asyncio.sleep(5)代替time.sleep(5)。无法避免的阻塞操作(CPU 密集、同步 IO)用
loop.run_in_executor丢到线程池。
7. GIL 概念初识(对比 JS 单线程)
JS 单线程:整个 JS 运行时(一个事件循环)只用一个线程,但 IO 交给底层(libuv 线程池),所以异步 IO 不阻塞。
Python 的 GIL(全局解释器锁):CPython 解释器在同一时刻只允许一个线程执行 Python 字节码。即使开了多线程,CPU 密集计算也不会真正并行。
关键区别:
结论:IO 密集(网络请求、文件读写)用 asyncio;CPU 密集(计算)用 multiprocessing,别指望多线程提速。
代码示例
并发抓取多个 URL
// JS
async function fetchAll(urls) {
const results = await Promise.all(
urls.map(u => fetch(u).then(r => r.json()))
);
return results;
}
# Python
import asyncio
async def fetch_all(urls):
results = await asyncio.gather(
*[fetch_json(u) for u in urls]
)
return results
异步定时器 vs setTimeout
// JS
await new Promise(r => setTimeout(r, 1000));
# Python
await asyncio.sleep(1)
带异常处理的并发
// JS
const results = await Promise.allSettled(urls.map(fetch));
# Python
results = await asyncio.gather(
*[fetch(u) for u in urls],
return_exceptions=True,
)
最佳实践
异步程序的入口统一用
asyncio.run(main()),并加if __name__ == "__main__":守卫。要并发就
asyncio.gather(或create_task),不要用for循环逐个await(那是串行)。异步函数内只用
await asyncio.sleep,别用time.sleep。IO 密集用
asyncio,CPU 密集用multiprocessing,别用多线程做 CPU 计算。大量并发请求配合
asyncio.Semaphore限流(类似 JS 里的并发限制)。记不住
asyncio.run时,优先用现代 HTTP 库(httpx)自带的 async client,能少踩很多坑。
前端开发者常见陷阱
调用 async 函数忘了 await/run:
fetch_data(url)不会执行,只是创建 coroutine,还伴随RuntimeWarning: coroutine was never awaited。忘记
asyncio.run()入口:没有事件循环就await,报SyntaxError(模块顶层)或RuntimeError。顶层 await 不存在:Python 模块顶层不能写
await,必须包进async def。用
for串行而非gather并发:for u in urls: await fetch(u)是串行,性能差一个数量级。阻塞调用卡死 loop:
time.sleep、同步requests.get、大量 CPU 计算都会卡住事件循环。Task 异常被静默吞掉:
create_task的任务若不被 await,其异常可能被忽略(JS 的 unhandled rejection 类比)。混用同步库和异步库:
requests(同步)和asyncio不兼容,要用httpx的 async 接口。以为多线程能加速 CPU 计算:GIL 使多线程对 CPU 密集任务无效,要用多进程。
gather的*解包忘了:asyncio.gather(*tasks)需要*展开列表,直接传列表会报错。在 Jupyter/已有循环里再跑
asyncio.run:asyncio.run不能在已有运行中的事件循环里调用。
总结
Python 的
asyncio和 JS 事件循环是同一套心智模型:单线程 + 协作式并发。async def/await语法几乎一致,但调用 async 函数不执行,必须await或用asyncio.run。Promise.all↔asyncio.gather,create_task对应"后台启动一个 Promise"。事件循环里绝不能阻塞;IO 密集用 asyncio,CPU 密集用多进程。
GIL 类比 JS 单线程:它让 CPython 多线程无法真正并行 Python 代码,理解它才能选对并发方案。
0.7-异步与并发基础
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法