asyncio 是 Python 3.4+ 引入、3.7+ 大幅简化的异步编程框架。它让单线程处理上万个并发 IO 任务成为可能——网络爬虫、Web 服务、聊天服务器、实时数据流都是它的主场。这一篇讲清楚:什么是协程、async/await 怎么写、asyncio.run / gather / create_task 的区别、以及为什么 asyncio 比多线程更适合大并发。
一、异步的核心思想
同步:一次只做一件事,卡住就干等。
异步:一件事等 IO 时主动让出控制权,去做别的事,IO 好了再回来。
想象你在餐厅点了 3 道菜:
- 同步:点第 1 道,坐着等做好;再点第 2 道,坐着等……做完 3 道要 30 分钟;
- 异步:一次性下 3 道单,等哪道好了就上哪道。10 分钟就吃完。
asyncio 就是让你的程序像”下多道单”一样。
二、协程:async def
1 | import asyncio |
async def定义协程函数;- 调用它得到协程对象(不会立即执行);
await等待另一个协程完成(期间让出 CPU 给别的协程);asyncio.run(coro)启动事件循环,运行协程。
注意:不能在 async def 之外用 await(会 SyntaxError)。
三、并发运行:asyncio.gather
想让多个协程并发跑:
1 | import asyncio |
输出:
1 | A 开始 |
3 个任务并发跑,总耗时等于最慢的那个(不是三个相加)。
四、create_task:立即调度
1 | async def main(): |
create_task 让协程立即开始,await 只是等它结束。
gather vs create_task:
- 想统一等一批任务全部完成 →
gather; - 想任务立即跑,之后再取结果 →
create_task。
五、asyncio.sleep vs time.sleep
永远别在 async 函数里用 time.sleep——它会阻塞整个事件循环:
1 | async def bad(): |
六、await 只能等”可等待对象”
- 协程(
async def定义的函数调用返回); - Task(
create_task返回); - Future(底层原语);
- 实现了
__await__的自定义对象。
普通函数调用不能 await:
1 | async def main(): |
七、异步 HTTP:httpx / aiohttp
爬 100 个 URL 的对比:
1 | # 顺序 requests |
100 个并发几乎没成本——单线程!
八、超时
1 | try: |
九、错误处理
协程里的异常会传播到 await 处:
1 | async def bad(): |
gather 默认在任何一个协程抛异常时立即抛出,其他协程被取消:
1 | try: |
十、TaskGroup(Python 3.11+)
新语法,比 gather 更结构化:
1 | async def main(): |
十一、Semaphore:限并发
不想同时 10000 个请求打爆对方:
1 | sem = asyncio.Semaphore(20) # 最多 20 个并发 |
十二、锁 / 事件 / 队列
跟 threading 类似,asyncio 也有对应的同步原语:
1 | lock = asyncio.Lock() |
十三、在同步代码里跑异步
1 | # ✅ 一次性入口 |
Jupyter Notebook 里已经有 loop,用 await 直接跑或 nest_asyncio 补丁。
十四、把同步代码”变异步”
有些库只有同步 API,不能 await。放到线程池里跑:
1 | loop = asyncio.get_running_loop() |
这样阻塞的 requests 也能在异步程序里用(但慢得多,能用 httpx 就用 httpx)。
十五、asyncio vs threading vs multiprocessing
| 场景 | 推荐 |
|---|---|
| 大量 IO 并发(爬虫、Web、DB) | asyncio |
| 现有同步库为主的项目 | threading |
| CPU 密集计算 | multiprocessing |
| 简单的”下 20 个请求” | threading(更简单) |
| 长期运行的服务器(10000+ 并发) | asyncio |
十六、常见异步库
- httpx:HTTP,同步 + 异步;
- aiohttp:HTTP + WebSocket;
- aiofiles:文件 IO;
- asyncpg、aiomysql、motor:数据库;
- FastAPI:异步 Web 框架;
- redis-py(4.x+):Redis 支持 async。
十七、一个完整例子:并发爬虫
1 | # crawler.py |
100 个请求几秒钟就能跑完。
十八、常见陷阱
- 在 async 函数里用
time.sleep:卡死事件循环。用asyncio.sleep。 - await 一个普通函数:报错。只能 await 协程/Task/Future。
- 协程创建了不 await:
RuntimeWarning: coroutine never awaited。 - 在同步函数里用 async:只能通过
asyncio.run入口。 - 一个协程被 await 多次:只能 await 一次;多次 await 用
create_task包装。 - gather 里一个失败全挂:加
return_exceptions=True。 - CPU 密集协程:
await不会自动让出,一直占用事件循环。用run_in_executor。
十九、小结与延伸阅读
async def定义协程,await等待;asyncio.run(main())是入口;gather并发跑一批,create_task立即调度;- 用异步 IO 库(httpx、aiohttp、asyncpg);
- 阻塞函数用
asyncio.to_thread; - 限并发用
asyncio.Semaphore; - 超时用
asyncio.timeout或wait_for; - IO 密集用 asyncio;CPU 密集用 multiprocessing。
延伸阅读:
- asyncio:https://docs.python.org/zh-cn/3/library/asyncio.html
- Python 异步编程指南:https://realpython.com/async-io-python/
- FastAPI:https://fastapi.tiangolo.com/
下一篇也是最后一篇 类型注解 type hints 实战 我们讲怎么让 Python 代码”看起来是静态类型语言”。