当前位置:首页>python>Python协程实战:async/await和事件循环,一篇文章弄懂

Python协程实战:async/await和事件循环,一篇文章弄懂

  • 2026-10-11 08:24:59
Python协程实战:async/await和事件循环,一篇文章弄懂

上一篇文章我们聊了异步IO的概念——单线程、不等待、协作切换。

这一篇直接上代码,把 async、await、协程、事件循环这几个核心概念全部讲明白。


01 从一段同步代码说起

先看这段代码,它顺序执行三次count()函数,每次打印One、等1秒、打印Two:

import time

def count():
    print("One")
    time.sleep(1)
    print("Two")
    time.sleep(1)

def main():
    for _ in range(3):
        count()

if __name__ == "__main__":
    start = time.perf_counter()
    main()
    elapsed = time.perf_counter() - start
    print(f"总耗时: {elapsed:.2f} 秒")

跑一下:

One
Two
One
Two
One
Two
总耗时: 6.03 秒

3次 × 2秒 = 6秒,合情合理。

但你看——每次time.sleep(1)的时候,程序真的在傻等。CPU什么事情都没干,就等着那1秒结束。这1秒如果能用来干别的,速度不就上来了吗?


02 改成异步版本

这就是asyncio登场的时候。把上面的代码改成异步:

import asyncio

async def count():
    print("One")
    await asyncio.sleep(1)
    print("Two")
    await asyncio.sleep(1)

async def main():
    await asyncio.gather(count(), count(), count())

if __name__ == "__main__":
    import time
    start = time.perf_counter()
    asyncio.run(main())
    elapsed = time.perf_counter() - start
    print(f"总耗时: {elapsed:.2f} 秒")

看看输出:

One
One
One
Two
Two
Two
总耗时: 2.00 秒

三个"One"同时打印出来,然后过1秒,三个"Two"又同时打印了。

这就是异步最直观的效果:不等。asyncio.sleep(1)不像time.sleep(1)那样阻塞一切,而是说"我暂停1秒,让别人先跑"——这就是await的关键作用。


03 协程函数 vs 协程对象

在上面的代码里,出现了几个关键元素:

协程函数——用async def定义的函数:

async def count():  # 这是协程函数
    ...

协程对象——调用协程函数得到的结果:

routine = count()  # 返回一个协程对象,不会执行
print(routine)     # <coroutine object count at 0x...>

这里有新手最常见的坑:

count()  # 不会执行!只是创建了一个协程对象

你以为调用了函数,但函数体根本没执行。要真正跑起来,必须用await或者在事件循环里调度它。

await count()       # ✅ 正确
asyncio.run(count())  # ✅ 或者用 run()

记着:协程函数本身不做任何事,直到你告诉事件循环去跑它。


04 async/await 的规则

async和await是一对搭档,它们有严格的语法规则:

async def f(x):
    y = await z(x)   # ✅ 协程里可以用 await 和 return
    return y

async def g(x):
    yield x          # ✅ 这是异步生成器

async def m(x):
    yield from gen(x)  # ❌ 不可以!SyntaxError

def n(x):
    y = await z(x)   # ❌ 不可以!非 async 函数里不能用 await
    return y

画重点:

  • await只能在async def的函数体内使用
  • async def里不能使用yield from
  • 如果你在async def里用了yield,那就是异步生成器,要用async for来迭代

await后面的东西必须是一个可等待对象(awaitable)——最常见的就是另一个协程。


05 事件循环:异步的调度中心

上面代码中的asyncio.run(main())就是启动事件循环的标准方式。

事件循环像是一个永不停止的调度器,它:

  1. 监控所有协程的状态
  2. 把准备好了的协程拿来执行
  3. 遇到await就暂停当前协程,切换到其他就绪的协程
  4. 等待的东西回来后,恢复对应的协程

不用写复杂的事件循环代码。在现代Python里,asyncio.run()帮你搞定一切——它获取事件循环、运行任务直到完成、最后关闭循环。

如果你想拿到当前运行的事件循环实例:

loop = asyncio.get_running_loop()

注意:如果没有事件循环在运行,这个函数会抛出RuntimeError。只适合在协程内部或事件循环已启动的上下文中调用。

事件循环的另一个重要特点是:它是可替换的。你可以用第三方的事件循环实现,比如有名的uvloop——一个基于libuv的替代品,号称比asyncio默认循环更快。

平台方面,Unix上默认是SelectorEventLoop,Windows上是ProactorEventLoop,后者对子进程和IO的兼容性更好。


06 asyncio REPL:交互式调试利器

从Python 3.8开始,你可以在终端直接体验异步代码:

python -m asyncio

你会进入一个特殊的REPL,可以直接在最顶层使用await:

>>> import asyncio

>>> async def main():
...     print("Hello...")
...     await asyncio.sleep(1)
...     print("World!")
...

>>> await main()   # 不需要 asyncio.run()
Hello...
World!

这在调试和测试异步代码时非常方便:不需要写一堆样板代码把协程包起来。


小结

这一篇我们写了异步IO的Hello World,搞清楚了协程、async/await、事件循环的基本关系。

回顾一下几个最重要的点:

  • async def定义协程函数,调用后得到协程对象
  • await暂停当前协程,让事件循环去调度别的协程
  • asyncio.run()启动事件循环
  • 协程不await不会执行,跟生成器一个道理

下一篇,我们玩点更复杂的——协程链式调用、生产者-消费者队列、async for/with、gather/create_task……把异步编程的实战能力拉满。


关注「Bug与灵光」,追更Python全系列教程。有问题欢迎留言,咱们下一篇见。

最新文章

随机文章