asyncio 事件循环:启动、停止、任务调度

事件循环是 asyncio 的发动机。你想想看,所有的协程、回调、IO 等待,最后都得靠这个循环来驱动。我自己刚开始学的时候,总以为事件循环是个很深奥的概念,用起来才发现,其实它就是一个不断轮询任务队列的循环——和 JavaScript 的 event loop 本质上差不多。

事件循环生命周期与任务调度 创建事件循环 运行中(调度任务) 停止 关闭 任务队列 create_task 添加 run_until_complete 阻塞 gather 并发等待 调用 run_forever 可重启

启动事件循环

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()。

停止事件循环

停止循环不是直接关闭线程,得按步骤来。我个人总结了三步走:

  1. 调用 loop.stop() —— 设置停止标志,循环会在下次检查时退出。
  2. 确保所有任务完成或取消 —— 否则会有未完成任务的警告。
  3. 调用 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 的灵魂。理解了它的启动、停止和调度,就等于拿到了异步编程的钥匙。接下来你自然会碰到如何设计同步与异步混合调用的接口,那就是咱们这个课程的核心实战了。