先建立一个贯穿全文的心智模型
在深入任何语法细节之前,请先记住一个隐喻,因为后面所有概念都可以用它来理解。
想象一个餐厅厨房。厨房里只有一个厨师(这就是 Python 的主线程 / 事件循环线程)。这个厨师同一时刻只能做一件事——切菜、翻锅、摆盘,不能真正"同时"做两件事。
但这个厨师很聪明。当他把一锅汤放到炉子上等它烧开的时候,他不会傻站着等,而是转身去切另一道菜的配料。等汤烧开了,炉子会发出提示音,他再回来处理汤。
这就是 Python asyncio 的本质:一个线程,通过"在等待的时候去做别的事"来实现高效的并发。这里的关键词是"并发"而不是"并行"——厨师始终只有一个人,但他通过合理安排时间,让多道菜同时推进。
await 就是厨师说"这一步我要等,先去做别的"的那个动作。如果一段代码没有 await,就相当于厨师从头到尾盯着一锅汤直到烧开——别的菜全部停摆。
记住这个画面,下面所有概念都会变得直觉化。
第一部分:迭代体系——“怎样一个一个地拿到值”
为什么需要迭代协议
假设你有一百万条数据需要处理。最暴力的方式是把它们全部加载到一个列表里,但这会吃掉大量内存。更聪明的方式是"要一条取一条"——你不需要同时持有全部数据,只需要一个机制能让你说"给我下一个"。
这就是迭代协议存在的原因。它和 Java 的 Iterator 接口本质上解决的是同一个问题,只不过 Python 的实现更轻量,语法糖更多。
三个核心角色
1️⃣ Iterable(可迭代对象) 是最宽泛的概念,它的意思就是"这个东西可以被遍历"。在 Python 里,一个对象只要实现了 __iter__() 方法,它就是 Iterable。list、tuple、dict、str、set 全都是。类比 Java,它相当于实现了 Iterable<T> 接口的对象。
2️⃣ Iterator(迭代器) 是真正干活的角色,它知道"当前遍历到哪了"以及"下一个值是什么"。它必须实现两个方法:__iter__()(返回自身)和 __next__()(返回下一个值,没有值了就抛 StopIteration)。类比 Java 的 Iterator<T>,__next__() 对应 next(),StopIteration 对应 hasNext() 返回 false。
一个关键理解点:Iterable 是"工厂",Iterator 是"游标"。你对一个列表调用 iter() 可以拿到多个独立的 Iterator,每个都有自己的位置状态,互不干扰。但 Iterator 本身调用 iter() 返回的是自己——它既是工厂也是游标,而且是一次性的。
先看列表为什么像“工厂”
例如:
lst = [10, 20, 30]
it1 = iter(lst)
it2 = iter(lst)
这里:
lst是Iterableit1、it2是两个不同的Iterator
它们各自维护自己的遍历位置:
next(it1) # 10
next(it1) # 20
next(it2) # 10
你会发现:
it1已经走到第二个元素了it2还是从头开始
这就像“同一本书,可以拿出两个书签分别夹在不同页”:
lst是那本书本身it1、it2是两个不同书签位置
所以 lst 更像“生成迭代器的来源”,也就是所谓的“工厂”。
再看 Iterator 为什么像“游标”
继续上面的例子,it1 本身就带着当前位置。
每次 next(it1),它都会往后走:
it1 = iter(lst)
next(it1) # 10
next(it1) # 20
next(it1) # 30
这个 it1 就像数据库游标、文件读取指针、播放器进度条:
- 它不是“数据集合本身”
- 它是“在数据集合上的当前位置”
所以叫“游标”很形象。
为什么说 Iterator 调用 iter() 会返回自己
这是 Python 迭代协议的一部分。
对一个迭代器再调用 iter(),结果还是它自己:
it = iter([10, 20, 30])
iter(it) is it # True
原因是:
- 它已经是“正在迭代的对象”了
- 不需要再制造一个新的迭代器
- 它自己就能继续往下走
这就是那句:
“Iterator 本身既是工厂也是游标。”
但这里“工厂”只是协议层面说它也能被 iter() 接受,不代表它像列表那样能反复产出全新独立迭代器。
为什么说 Iterator 是一次性的
因为它带状态,而且状态会前进。
例如:
it = iter([10, 20, 30])
list(it) # [10, 20, 30]
list(it) # []
第一次已经把它消耗完了,第二次就没了。
这就是“一次性”的意思。
而列表不是一次性的:
lst = [10, 20, 30]
list(lst) # [10, 20, 30]
list(lst) # [10, 20, 30]
因为每次 iter(lst) 都能重新拿到一个新的迭代器。
最关键的差别
可以用这两段对比来记。
Iterable:
lst = [1, 2, 3]
iter(lst) is iter(lst) # False
通常每次都能拿到新的迭代器。
Iterator:
it = iter(lst)
iter(it) is it # True
对自己再 iter(),还是自己。
3️⃣ Generator(生成器) 是一种特殊的 Iterator,它用 yield 关键字来定义。它的特殊之处在于:函数体不会一次性执行完,而是每次执行到 yield 就暂停,把值交出去,下次调用 next() 时从暂停的地方继续执行。
def countdown(n):
while n > 0:
yield n # 执行到这里暂停,把 n 交出去
n -= 1 # 下次 next() 从这里继续
for num in countdown(3):
print(num) # 依次打印 3, 2, 1
这三者的继承关系是:Generator 是一种 Iterator,Iterator 是一种 Iterable。换句话说,所有生成器都可以被 for 循环消费,但不是所有可迭代对象都是生成器。
for 循环的真面目
当你写 for x in something: 的时候,Python 在背后做的事情大致等价于:
_iter = iter(something) # 调用 __iter__(),拿到迭代器
while True:
try:
x = next(_iter) # 调用 __next__(),拿下一个值
except StopIteration:
break # 没有更多值了,退出
# ... 执行循环体 ...
所以 for 不是什么魔法,它只是迭代协议的语法糖。理解了这一点,后面理解 async for 就自然了。
第二部分:异步的本质——“在等待的时候去做别的”
为什么需要异步
回到厨师的比喻。假设你的程序需要同时处理 100 个网络请求。每个请求发出后,要等几百毫秒到几秒才能收到响应。如果用同步方式,厨师就得"发一个请求 → 傻等响应 → 处理 → 发下一个请求",绝大部分时间都浪费在等待上。
异步的核心思想是:发出请求后不傻等,先去处理别的请求,等响应到了再回来继续。这对 I/O 密集型任务(网络请求、文件读写、数据库查询)效果极好,但对 CPU 密集型任务(大量数学计算)没什么帮助——因为 CPU 计算没有"等待"的间隙可以利用。
async def 的真正含义
async def 声明的函数叫做"协程函数"。但这里有一个非常容易产生的误解:async def 不会让函数里的代码自动变成异步的。它只是告诉 Python:“这个函数遵循异步协议,它的执行可以被暂停和恢复。”
异步协议是 Python 为异步执行定义的一组行为约定,目的是让解释器和事件循环知道如何等待、暂停、恢复和遍历异步对象。async def 的真正含义,不是让函数体自动变异步,而是声明这个函数返回协程对象,进入异步协议体系,可被事件循环调度。真正发生“让出执行权”的位置是 await。异步协议主要包括三类:__await__() 支持 await,表示对象可等待;__aiter__() 和 __anext__() 支持 async for,表示对象可异步迭代;__aenter__() 和 __aexit__() 支持 async with,表示对象可异步上下文管理。因此,所谓“遵循异步协议”,本质上就是对象实现了这些规则,能与 Python 异步语法和事件循环正确协作。
这就好比一个厨师穿上了写着"我会协调工作"的围裙,但如果他实际做事时从来不说"我这步要等一下,先去做别的",那围裙就只是一件衣服,毫无作用。
这是一个极其重要的反直觉点,值得用反面例子来强化。下面这个函数虽然是 async def,但它会完全卡死事件循环:
async def bad_example():
import time
time.sleep(5) # 这是同步阻塞!整个事件循环被冻结 5 秒
return "done"
正确的写法是用能配合事件循环的异步等待:
async def good_example():
import asyncio
await asyncio.sleep(5) # 挂起当前协程,事件循环去干别的,5 秒后回来
return "done"
总结成一句话:async def 给了函数"可以被暂停"的能力,但只有 await 才会真正触发暂停。
await 的本质
await 做的事情可以拆解为两步:
第一步,把当前协程的控制权交还给事件循环。这就是厨师说"汤在炉子上了,我去切菜"的那个时刻。
第二步,等所等待的那个操作完成后,事件循环会把结果送回来,协程从 await 的地方继续执行。这就是炉子发出提示音,厨师回来继续处理汤。
await 后面不能跟任意对象,只能跟"可等待对象"(awaitable)。最常见的三种可等待对象是协程对象(async def 函数调用后返回的东西)、asyncio.Task、和 asyncio.Future。从底层协议来说,任何实现了 __await__() 方法的对象都是可等待的,但在日常开发中你几乎不需要自己实现这个方法。
事件循环:那个唯一的厨师
事件循环(Event Loop)就是那个厨师。它的工作方式是一个无限循环:
- 从就绪队列里取出一个协程
- 执行这个协程,直到它遇到
await或者执行完毕 - 如果遇到
await,把协程挂起,记录它在等什么 - 检查有没有之前挂起的协程,它等的东西已经好了
- 如果有,恢复那个协程
- 回到第 1 步
整个过程在单线程里完成。这意味着任何一个协程如果不 await,就会独占这个线程,所有其他协程都得等着。这也是为什么在 async def 里写 time.sleep() 是灾难——它直接冻结了唯一的厨师。
第三部分:同步与异步的对应关系——一张完整的映射表
理解了前两部分之后,现在可以建立一张完整的对应关系了。Python 的迭代体系其实是两套平行结构:同步体系和异步体系。它们的概念一一对应,只是异步版本在每个"取值"操作上都加了"让出控制权"的能力。
函数的四种组合,这是最核心的区分:def + return 是普通同步函数,调用后直接得到返回值。def + yield 是同步生成器,调用后得到一个可以用 for 遍历的生成器对象。async def + return 是协程函数,调用后得到协程对象,必须用 await 来获取结果。async def + yield 是异步生成器,调用后得到一个可以用 async for 遍历的异步生成器对象。
这四个组合覆盖了你在实际代码中会遇到的所有情况。特别注意:yield 和 async 是两个独立的维度。yield 说的是"分多次产出值还是一次性返回",async 说的是"是否运行在异步协议里"。它们的组合产生了四种不同的东西。
迭代消费方式的对应也很对称:同步迭代器用 for 消费,背后调用的是 __iter__() 和 __next__()。异步迭代器用 async for 消费,背后调用的是 __aiter__() 和 __anext__()。两套机制结构完全一样,只是异步版本的 __anext__() 返回的是一个可等待对象,事件循环可以在等待下一个值的时候去做别的事。
第四部分:同步和异步的桥接——为什么需要线程池
实际工程中,你经常会遇到一个场景:你的主程序是异步的(比如一个 ASGI Web 框架),但你调用的某个库只提供同步接口(比如一个同步的数据库驱动,或者一个返回同步迭代器的 SDK)。这时候就出现了一个棘手的问题:你不能直接在事件循环线程里调用同步阻塞代码,否则整个事件循环会被卡住。
解决方案是把同步阻塞操作扔到线程池里执行。事件循环线程本身不会被阻塞,它只是 await 线程池的结果。
Starlette 框架里的 iterate_in_threadpool 就是这个模式的经典实现:
async def iterate_in_threadpool(iterator: Iterable[T]) -> AsyncIterator[T]:
as_iterator = iter(iterator) # 拿到同步迭代器
while True:
try:
yield await anyio.to_thread.run_sync(_next, as_iterator)
# 关键:next() 这个可能阻塞的操作在线程池里执行
# await 等待线程池的结果,期间事件循环可以去做别的
except _StopIteration:
break
逐行理解这段代码:iter(iterator) 把可迭代对象转成迭代器。anyio.to_thread.run_sync(_next, as_iterator) 把 next(as_iterator) 这个同步调用丢到线程池里执行,返回一个可等待对象。await 等待线程池完成,期间不阻塞事件循环。yield 把拿到的值产出给调用者。
整个函数是 async def + yield,所以它是异步生成器,对外表现为 AsyncIterator。调用者可以用 async for 来消费它,完全感觉不到底层其实有一个同步迭代器在线程池里工作。
这揭示了一条重要的工程原则:async 负责调度协议,线程池负责隔离阻塞代码。两者是协作关系,不是替代关系。 异步框架不能消灭阻塞——它只能把阻塞隔离到不会影响事件循环的地方。
第五部分:八条核心规则(速查用)
yield只说明"分步产出值",和异步没有任何关系。def + yield是同步生成器,async def + yield才是异步生成器。async def只说明"这个函数遵循异步协议",不保证内部代码不阻塞。 在async def里写time.sleep()照样会冻结事件循环。- 真正让协程让出执行权的是
await。 没有await的async def和普通函数在执行效果上没有区别。 await只能等待可等待对象。 最常见的是协程对象、Task和Future。底层靠__await__()协议。for对应同步迭代,async for对应异步迭代。 两者结构对称,不要混用。- 事件循环是单线程的。 任何在事件循环线程里的阻塞操作都会卡住所有协程。
- 同步阻塞代码需要通过线程池桥接进异步系统。
asyncio.to_thread()或anyio.to_thread.run_sync()是标准做法。 - Generator 是 Iterator 的特例,Iterator 是 Iterable 的特例。 同样地,
AsyncGenerator是AsyncIterator的特例,AsyncIterator是AsyncIterable的特例。