上一篇文章我们聊了异步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
画重点:
- 如果你在
async def里用了yield,那就是异步生成器,要用async for来迭代
await后面的东西必须是一个可等待对象(awaitable)——最常见的就是另一个协程。
05 事件循环:异步的调度中心
上面代码中的asyncio.run(main())就是启动事件循环的标准方式。
事件循环像是一个永不停止的调度器,它:
- 遇到
await就暂停当前协程,切换到其他就绪的协程
不用写复杂的事件循环代码。在现代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定义协程函数,调用后得到协程对象
下一篇,我们玩点更复杂的——协程链式调用、生产者-消费者队列、async for/with、gather/create_task……把异步编程的实战能力拉满。
关注「Bug与灵光」,追更Python全系列教程。有问题欢迎留言,咱们下一篇见。