asyncio 事件循环:启动、停止、任务调度
事件循环是 asyncio 的发动机。你想想看,所有的协程、回调、IO 等待,最后都得靠这个循环来驱动。我自己刚开始学的时候,总以为事件循环是个很深奥的概念,用起来才发现,其实它就是一个不断轮询任务队列的循环——和 JavaScript 的 event loop 本质上差不多。
启动事件循环
Python 3.7 之后,启动事件循环最简单的方式就是 asyncio.run()。我个人习惯在小型脚本或测试代码里直接用它,又短又干净。
import asyncio
async def main():
await asyncio.sleep(1)
print("搞定")
asyncio.run(main()) # 一行启动,自动创建和关闭循环
但要注意,asyncio.run() 每次都会创建新的事件循环,并且跑完就关。如果需要在同一个循环里多次执行任务,就得用更底层的方式。
| 启动方式 | 适用场景 | 注意点 |
|---|---|---|
asyncio.run() | 入口函数、一次性脚本 | 自动创建/关闭,不能嵌套调用 |
loop.run_until_complete() | 需要自定义循环时 | 阻塞直到协程完成,之后循环仍在运行 |
loop.run_forever() | 长时间运行的服务 | 需要手动 stop() 才能退出 |
asyncio.get_event_loop() | 获取当前线程事件循环 | Python 3.10+ 已废弃该用法 |
我曾经踩过这个坑:在 Jupyter Notebook 里直接调用
asyncio.run() 会报错——因为 Notebook 自己已经在跑一个事件循环了。正确做法是用 await 或者 loop = asyncio.get_event_loop(); loop.run_until_complete()。
停止事件循环
停止循环不是直接关闭线程,得按步骤来。我个人总结了三步走:
- 调用
loop.stop()—— 设置停止标志,循环会在下次检查时退出。 - 确保所有任务完成或取消 —— 否则会有未完成任务的警告。
- 调用
loop.close()—— 释放资源,关闭连接。
loop = asyncio.new_event_loop()
try:
loop.run_forever() # 如果有任务在跑,先停止
except KeyboardInterrupt:
loop.stop() # 第一步
finally:
# 取消剩余任务
pending = asyncio.all_tasks(loop=loop)
for task in pending:
task.cancel()
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
loop.close() # 第三步
小技巧:停止后如果想重新运行同一个循环,可以再次调用
loop.run_forever() 而不用新建。但 close() 之后就不能再用了。
任务调度
说白了,事件循环就是靠调度器决定“现在该跑哪个协程”。我自己在写爬虫项目时,经常用这三种调度方式:
create_task—— 把协程包装成 Task 并安排到事件循环里,是异步并发的核心。asyncio.gather—— 并发运行多个协程,等所有结果返回。我习惯用它做批量请求。asyncio.wait—— 更灵活的等待策略,可以设置超时或等待第一个完成。
async def fetch(url):
await asyncio.sleep(1) # 模拟网络请求
return f"数据:{url}"
async def main():
# 方法1:create_task 分别调度
task1 = asyncio.create_task(fetch("url1"))
task2 = asyncio.create_task(fetch("url2"))
result1 = await task1
result2 = await task2
# 方法2:gather 统一调度
results = await asyncio.gather(
fetch("url3"),
fetch("url4"),
fetch("url5")
)
# 方法3:wait 带超时
done, pending = await asyncio.wait(
[fetch("url6"), fetch("url7")],
timeout=2.0
)
for task in done:
print(task.result())
asyncio.run(main())
事件循环的内部调度机制
很多朋友问我:事件循环怎么知道哪个协程先跑?其实核心就是 ready 队列 和 waiting 队列。协程遇到 await 时挂起,把 future 注册到等待队列;IO 完成后回调会把协程放回 ready 队列。循环每次迭代只处理 ready 队列里的任务,然后检查是否有超时等事件。
记住这个原则:协程只有在
await 处才会让出控制权。如果你写了一个死循环 while True: pass,事件循环就永远轮不到其他任务。所以我一直建议「不要写阻塞代码」。
避坑指南
- 不要在异步函数里混合使用
time.sleep()—— 它会阻塞整个线程,用await asyncio.sleep()。 - 不要在事件循环外调用
loop的方法 —— 在子线程里创建新循环时务必用asyncio.new_event_loop()。 - 我在一个微信机器人项目里遇到过:创建了大量 Task 但忘记
await,导致任务被垃圾回收,控制台什么输出都没有。你想想看,排查了好几个小时才发现是引用丢失。所以一定要保留 Task 对象的引用,或者用asyncio.gather统一管理。
事件循环是 asyncio 的灵魂。理解了它的启动、停止和调度,就等于拿到了异步编程的钥匙。接下来你自然会碰到如何设计同步与异步混合调用的接口,那就是咱们这个课程的核心实战了。