前言
很多人第一次听到「异步编程」,会以为它就是「更快的多线程」,或者以为只要把函数前面加上async就自动变快。这两种理解都不准确。异步(asynchronous)要解决的核心问题只有一个:当程序在等待 I/O(网络请求、磁盘读写、子进程输出)时,不要把整个线程干等着。
同步代码里,一次网络请求要等几百毫秒,这段时间 CPU 什么都没干,线程被阻塞。异步的思路是把「等待」这件事交给一个调度中心——事件循环(event loop),让它在等待 A 的时候去跑 B。所以异步不是让单个操作变快,而是让等待重叠起来。
本文讲四件事:事件循环到底在做什么、async def/await的语法含义、协程与生成器的历史渊源(yield from怎么变成await的),以及为什么一段忘了await的同步代码会把整个事件循环卡死。文中所有示例都以 Python 3 为基准,标注了适用版本。
一、事件循环:异步程序的调度中心
asyncio的核心是一个事件循环。你可以把它想成一个永不停止的while True循环,每一轮做三件事:
- 检查哪些「可等待对象」(awaitable)已经就绪——比如某个 socket 有数据可读了;
- 把就绪的回调 / 协程推进下一步;
- 如果没有就绪的任务,就让操作系统把线程挂起,直到有事件发生(这一步靠
select/epoll/kqueue之类的 I/O 多路复用)。
关键点在于:整个事件循环跑在一个线程里。它靠「协作式切换」而不是「抢占式切换」——也就是说,一个协程必须主动在await处让出控制权,事件循环才能去跑别的协程。这和线程完全不同:线程由操作系统强制打断,协程是自己让位。
# 适用于 Python 3.7+
import asyncio
async def main():
print("开始")
await asyncio.sleep(1) # 在这里让出控制权,事件循环可以去跑别的任务
print("一秒后")
asyncio.run(main()) # asyncio.run() 是 3.7 新增asyncio.run(main())做了三件事:创建一个新的事件循环、把main()作为入口协程跑完、最后关闭循环。它是 3.7 引入的,替代了早期啰嗦的loop = asyncio.get_event_loop(); loop.run_until_complete(...)写法。在 3.10 起,如果没有正在运行的事件循环,asyncio.get_event_loop()会发出DeprecationWarning,所以新代码应当直接用asyncio.run()。
二、async def 与 await 的语法含义
async def定义一个协程函数(coroutine function)。注意一个容易踩的点:
# 适用于 Python 3.8+
async def fetch():
return 42
print(fetch) # <function fetch at ...>,是一个普通函数对象
print(fetch()) # <coroutine object fetch at ...>,调用后得到协程对象调用async def函数不会执行函数体,只是创建一个协程对象。函数体要等这个协程被await、被asyncio.create_task()调度,或者被事件循环驱动时才真正开始运行。如果你只是调用它却从不 await,Python 会给出RuntimeWarning: coroutine 'fetch' was never awaited。
await只能出现在async def内部,它的含义是:「把这个可等待对象交给事件循环,等它完成后再拿回结果,等待期间我可以让位」。能被await的东西有三类:
- 协程对象(
async def调用的返回值); asyncio.Task/asyncio.Future;- 任何实现了
__await__的对象(比如asyncio.Queue.get()返回的对象)。
await和普通函数调用在写法上很像,但语义完全不同:普通调用会一直占用线程直到返回,await会在真正等待时挂起当前协程。
三、从 yield from 到 await:协程的历史
Python 的协程并不是凭空出现的,它和生成器(generator)是同一条演化线上的产物。
- Python 2.5给生成器加上了
send()和yield表达式,于是生成器第一次具备了「双向通信」的能力,PEP 342 称之为「增强型生成器」。 - Python 3.3引入
yield from(PEP 380),语法上可以方便地把一个生成器的产出委托给另一个,这为「协程库」提供了基础,asyncio早期就建立在它之上。 - Python 3.4
asyncio进入标准库,用@asyncio.coroutine装饰器和yield from写协程。 - Python 3.5引入
async/await关键字(PEP 492),把「生成器」和「协程」在语法上区分开,yield from在协程里的角色被await取代。
所以那句「协程是特殊的生成器」在 3.5 之前基本成立,之后就不严谨了——现在它们是两种不同的对象,async def产出的是原生协程(native coroutine),def+yield产出的是生成器。二者的共同点是都靠「挂起与恢复」工作。
# 适用于 Python 3.5+(旧写法在 3.4 用 @asyncio.coroutine + yield from)
import asyncio
# 生成器写法(旧):靠 yield from 委托
def old_style():
yield from asyncio.sleep(1)
# 原生协程写法(新):async def + await
async def new_style():
await asyncio.sleep(1)@asyncio.coroutine在 Python 3.11 中已被移除,旧代码必须迁移到async def。这也是为什么看老资料时要注意年代。
四、同步代码为什么会阻塞事件循环
这是初学者最常见的误解来源:一个协程里如果调用了阻塞函数,整个事件循环都会停住,因为事件循环只有一个线程。
# 适用于 Python 3.8+
import asyncio, time
async def bad():
time.sleep(2) # ❌ 阻塞调用:整个事件循环停 2 秒
return "done"
async def good():
await asyncio.sleep(2) # ✅ 挂起当前协程,让出控制权
return "done"time.sleep()和asyncio.sleep()看起来都是「睡 2 秒」,但前者真的让线程停住,后者只是把当前协程标记为「2 秒后唤醒」,然后把控制权交还给事件循环。所以异步程序里要避免:
time.sleep()→ 用await asyncio.sleep();requests.get()这类同步 HTTP 库 → 用异步 HTTP 客户端,或者临时用asyncio.to_thread()(3.9 新增)把它扔到线程里;open()的同步读写、耗时的 CPU 计算 → 用asyncio.to_thread()或干脆放进程池。
asyncio.to_thread()是 Python 3.9 才有的,3.8 及更早版本要用loop.run_in_executor(None, func, *args)代替。
常见坑点
- 调用协程函数却忘了 await
❌fetch()(只创建了协程对象,函数体根本不跑,还收到 never awaited 警告)
✅await fetch(),或asyncio.create_task(fetch())后由事件循环调度
- 在协程里用
time.sleep()阻塞
❌async def f(): time.sleep(1)
✅async def f(): await asyncio.sleep(1)
- 拿
asyncio.run()在已经运行的事件循环里再启动一次
❌ 在协程内部写asyncio.run(another()),会抛RuntimeError: asyncio.run() cannot be called from a running event loop
✅ 直接await another()
- 把
async def的函数当普通回调传给map/sorted的 key
❌list(map(fetch, urls))得到一堆没人 await 的协程对象
✅ 先tasks = [asyncio.create_task(fetch(u)) for u in urls],再await asyncio.gather(*tasks)
- 以为加了
async就等于并发
❌await f(); await g()(这是串行,g 要等 f 完成)
✅await asyncio.gather(f(), g()),或先create_task再统一等待
- 在
async def里用同步的文件 I/O 或 CPU 计算
❌async def f(): return sum(range(10**8))
✅ 用await asyncio.to_thread(...)(3.9+)或交给ProcessPoolExecutor
- 依赖
asyncio.get_event_loop()获取循环
❌loop = asyncio.get_event_loop()在现代代码里可能抛出 DeprecationWarning
✅ 顶层用asyncio.run(main()),需要循环对象时在协程内用asyncio.get_running_loop()
总结
| 概念 | 一句话理解 | 关键点 |
|---|
| 事件循环 | 单线程里的调度中心 | 协作式切换,靠 await 让位 |
async def | 定义协程函数 | 调用只得到协程对象,不执行函数体 |
await | 等待可等待对象 | 等待期间让出控制权 |
yield from→await | 生成器到原生协程的演化 | 3.5 起语法区分,旧写法 3.11 移除 |
| 阻塞调用 | 卡死事件循环的元凶 | 用异步等价物或 to_thread 隔离 |
异步的本质是「用一个线程调度大量等待中的任务」,它擅长 I/O 密集,对 CPU 密集没有帮助——CPU 密集要靠多进程。入门时先把事件循环和await的语义吃透,比急着背 API 重要得多。