Python asyncio 模块学习记录:从"等着"到"切出去干点别的"
最近在补 Python 的异步编程,绕不开 asyncio。一开始我对它的理解挺模糊:async、await、协程、事件循环、Task,这些词看起来都认识,但放在一起就有点飘。
这篇文章算是一份学习记录。我主要理清 asyncio 主要的几个问题:它解决什么问题,核心概念是什么,代码应该怎么写,以及有哪些容易踩坑的地方。

1. asyncio 解决的是什么问题?
先说结论:asyncio 主要适合 I/O 密集型任务。
比如:
- 网络请求
- 数据库查询
- 文件读写
- WebSocket 通信
- 定时任务
- 爬虫
- 聊天服务
- 高并发接口调用
这些任务的共同点是:程序经常不是在"计算",而是在"等待"。
举个很普通的例子:请求 3 个接口,每个接口都要等 2 秒。
同步写法大概是这样:
python
import time
def fetch(name):
print(f"开始请求 {name}")
time.sleep(2)
print(f"完成请求 {name}")
fetch("A")
fetch("B")
fetch("C")
总耗时大约 6 秒,因为它是一个接一个等。
但异步的想法是:
既然 A 在等网络返回,那我为什么不先去处理 B?B 也在等,那我再去处理 C。
也就是说,asyncio 并不是让 Python 同时做很多计算,而是让程序在等待的时候别傻站着。
2. 一个简单的异步例子
先看代码:
python
import asyncio
async def fetch(name):
print(f"开始请求 {name}")
await asyncio.sleep(2)
print(f"完成请求 {name}")
async def main():
await asyncio.gather(
fetch("A"),
fetch("B"),
fetch("C"),
)
asyncio.run(main())
这段代码总耗时大约 2 秒,而不是 6 秒。
注意这里有几个关键词:
async def:定义一个协程函数await:等待一个异步操作完成asyncio.sleep():异步版本的 sleepasyncio.gather():并发运行多个协程asyncio.run():启动事件循环,运行入口协程
这就是 asyncio 最基础的味道。
3. 协程是什么?
普通函数调用后会立刻执行:
python
def hello():
print("hello")
hello()
但协程函数不一样:
python
async def hello():
print("hello")
hello()
你会发现,直接调用 hello() 并不会真正执行函数体,它只是返回了一个协程对象。
真正执行它,需要:
python
await hello()
或者在最外层:
python
asyncio.run(hello())
可以先粗略理解成:
协程是一种可以暂停和恢复的函数。
当协程执行到 await 时,它会把控制权交还给事件循环。事件循环发现这个任务暂时在等,就会去调度别的任务。
4. 事件循环:asyncio 的调度中心
事件循环可以理解成 asyncio 的"管家"。它不断检查:
- 哪些任务可以运行?
- 哪些任务正在等待?
- 哪些任务已经完成?
- 等待完成后该恢复哪个任务?
流程大概是这样:

5. await 到底在等什么?
await 后面通常接的是一个"可等待对象",常见有:
- 协程对象
- Task
- Future
- 一些异步库返回的对象
比如:
python
await asyncio.sleep(1)
这里不是让线程阻塞 1 秒,而是告诉事件循环:
当前协程先暂停 1 秒,这段时间你可以去执行别的任务。
这点很关键。
如果在异步函数里写:
python
time.sleep(1)
那就糟了。它会阻塞整个线程,事件循环也动不了,其他协程也没法执行。
正确写法是:
python
await asyncio.sleep(1)
6. create_task:把协程变成任务
看一个容易误解的例子:
python
async def work(name):
print(f"{name} start")
await asyncio.sleep(1)
print(f"{name} end")
async def main():
await work("A")
await work("B")
这段代码虽然用了 async,但仍然是顺序执行。因为它先等 A 完成,再等 B 完成。
如果想让 A 和 B 同时开始,需要创建任务:
python
async def main():
task_a = asyncio.create_task(work("A"))
task_b = asyncio.create_task(work("B"))
await task_a
await task_b
也可以写成:
python
async def main():
await asyncio.gather(
work("A"),
work("B"),
)
我的理解是:
- 协程像"任务说明书"
- Task 像"已经交给事件循环执行的任务"
只定义协程不代表它已经被调度执行。
7. gather:等一组任务全部完成
asyncio.gather() 很适合批量并发。
python
import asyncio
async def download(index):
print(f"开始下载文件 {index}")
await asyncio.sleep(1)
print(f"下载完成文件 {index}")
return f"file-{index}"
async def main():
results = await asyncio.gather(
download(1),
download(2),
download(3),
)
print(results)
asyncio.run(main())
输出类似:
text
开始下载文件 1
开始下载文件 2
开始下载文件 3
下载完成文件 1
下载完成文件 2
下载完成文件 3
['file-1', 'file-2', 'file-3']
gather() 的特点是:
它会等所有任务完成,并按照传入顺序返回结果。
8. 一个更贴近实际的例子:批量处理请求
这里不用真实网络请求,先用 asyncio.sleep() 模拟接口耗时。
python
import asyncio
import random
async def request_api(user_id):
delay = random.uniform(0.5, 2)
print(f"请求用户 {user_id},预计耗时 {delay:.2f}s")
await asyncio.sleep(delay)
return {
"user_id": user_id,
"status": "ok",
}
async def main():
user_ids = [101, 102, 103, 104, 105]
tasks = [
request_api(user_id)
for user_id in user_ids
]
results = await asyncio.gather(*tasks)
for item in results:
print(item)
asyncio.run(main())
这类场景在真实项目里很常见:比如批量查用户信息、批量调用第三方接口、批量拉取远程资源。
不过这里也引出一个问题:如果一次性创建几千个任务,会不会把服务打爆?
答案是:会有风险。
这时候就需要限制并发数量,避免造成较大的访问量,服务器宕机。
9. 用 Semaphore 控制并发数量
Semaphore 可以控制同一时间最多有多少个任务在执行某段逻辑。
python
import asyncio
import random
async def request_api(user_id, sem):
async with sem:
delay = random.uniform(0.5, 2)
print(f"开始请求用户 {user_id}")
await asyncio.sleep(delay)
print(f"完成请求用户 {user_id}")
return user_id
async def main():
sem = asyncio.Semaphore(3)
tasks = [
request_api(user_id, sem)
for user_id in range(1, 11)
]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
这里 Semaphore(3) 表示最多同时执行 3 个请求。
这个写法很实用。写爬虫、批量请求接口、批量处理消息时,经常能用上。
10 个任务
Semaphore 限制
同时最多运行 3 个
任务完成后释放名额
后续任务继续进入

10. 超时处理:asyncio.wait_for
异步任务里,超时控制很重要。比如请求第三方接口,不可能无限等下去。
python
import asyncio
async def slow_request():
await asyncio.sleep(5)
return "done"
async def main():
try:
result = await asyncio.wait_for(slow_request(), timeout=2)
print(result)
except asyncio.TimeoutError:
print("请求超时")
asyncio.run(main())
这里 slow_request() 要 5 秒,但 wait_for() 只等 2 秒,所以会抛出 TimeoutError。
这类代码在实际项目里很常见,因为异步程序如果缺少超时,可能会堆积大量迟迟不结束的任务。
11. 取消任务:task.cancel()
任务被创建后,也可以取消。
python
import asyncio
async def worker():
try:
while True:
print("working...")
await asyncio.sleep(1)
except asyncio.CancelledError:
print("任务被取消,开始清理资源")
raise
async def main():
task = asyncio.create_task(worker())
await asyncio.sleep(3)
task.cancel()
try:
await task
except asyncio.CancelledError:
print("main 捕获取消结果")
asyncio.run(main())
这里要注意:
CancelledError 不只是一个普通错误,它通常意味着任务生命周期要结束了。如果捕获它,一般清理完资源后要继续 raise,不要悄悄吞掉。
12. Queue:生产者和消费者模型
asyncio.Queue 很适合处理"一个地方生产任务,另一个地方消费任务"的场景。
python
import asyncio
import random
async def producer(queue):
for i in range(1, 6):
await asyncio.sleep(0.5)
item = f"task-{i}"
await queue.put(item)
print(f"生产 {item}")
async def consumer(queue, name):
while True:
item = await queue.get()
try:
print(f"{name} 处理 {item}")
await asyncio.sleep(random.uniform(0.5, 1.5))
finally:
queue.task_done()
async def main():
queue = asyncio.Queue()
producers = [
asyncio.create_task(producer(queue))
]
consumers = [
asyncio.create_task(consumer(queue, "worker-A")),
asyncio.create_task(consumer(queue, "worker-B")),
]
await asyncio.gather(*producers)
await queue.join()
for c in consumers:
c.cancel()
asyncio.run(main())
这个例子很像后台任务系统:
- producer 负责放入任务
- consumer 负责处理任务
- queue 负责缓冲任务

13. asyncio 适合什么,不适合什么?
适合:
- 高并发网络请求
- Web 服务
- WebSocket
- 爬虫
- 消息队列消费者
- 定时任务调度
- I/O 密集型后台任务
不太适合:
- 大量 CPU 计算
- 图像处理
- 视频编码
- 大规模数学运算
- 需要真正并行计算的场景
因为 asyncio 的并发主要发生在等待 I/O 的时候。
如果任务本身一直占着 CPU 算东西,它不会主动让出控制权,事件循环也没机会调度别的协程。
CPU 密集型任务通常更适合:
multiprocessingconcurrent.futures.ProcessPoolExecutor- C 扩展
- NumPy 这类释放 GIL 的计算库
14. 几个常见坑
坑 1:async 函数调用后没有 await
python
async def hello():
print("hello")
hello()
这样不会真正执行。要写:
python
await hello()
或者:
python
asyncio.run(hello())
坑 2:在 async 函数里用了 time.sleep
python
async def bad():
time.sleep(1)
这会阻塞事件循环。应该写:
python
async def good():
await asyncio.sleep(1)
坑 3:以为 async 就一定更快
如果任务是 CPU 密集型,asyncio 不一定更快,甚至可能更绕。
坑 4:忘了处理异常
python
task = asyncio.create_task(do_something())
如果创建了任务但后面不 await,它里面的异常可能不容易被及时发现。更稳妥的方式是保存 task,并在合适的地方等待或统一管理。
坑 5:在已经运行的事件循环里调用 asyncio.run
asyncio.run() 通常只应该作为程序入口调用一次。
在 Jupyter、某些 Web 框架或异步环境里,事件循环可能已经存在,这时再调用 asyncio.run() 就容易报错。
15. 总结
我对 asyncio 的理解大概是:
它不是让 Python 魔法般同时执行很多代码,而是提供了一套协作式调度机制,让程序在等待 I/O 的时候主动让出执行权。
也就是说,asyncio 的核心不是"快",而是"别浪费等待时间"。
它的几个关键词可以串起来理解:
async def
创建协程
create_task
交给事件循环调度
遇到 await 暂停
执行其他任务
I/O 完成后恢复
asyncio 刚开始看起来概念很多,但抓住几个点会清晰很多:
async def定义协程函数- 调用协程函数只会得到协程对象,不会立即执行
await会暂停当前协程,把控制权交给事件循环create_task()会把协程包装成任务并调度执行gather()可以等待多个任务完成Semaphore可以限制并发数量Queue适合生产者消费者模型- 不要在异步代码里写阻塞操作,比如
time.sleep()
目前我觉得,学习
asyncio最好的方式不是一上来背概念,而是多写几个小例子:模拟请求、限制并发、处理超时、取消任务、队列消费。写着写着,事件循环和协程之间的关系就会慢慢变得具体,通过几个例子的编写练习,尝试去融会贯通。