先建立一个贯穿全文的心智模型

在深入任何语法细节之前,请先记住一个隐喻,因为后面所有概念都可以用它来理解。

想象一个餐厅厨房。厨房里只有一个厨师(这就是 Python 的主线程 / 事件循环线程)。这个厨师同一时刻只能做一件事——切菜、翻锅、摆盘,不能真正"同时"做两件事。

但这个厨师很聪明。当他把一锅汤放到炉子上等它烧开的时候,他不会傻站着等,而是转身去切另一道菜的配料。等汤烧开了,炉子会发出提示音,他再回来处理汤。

这就是 Python asyncio 的本质:一个线程,通过"在等待的时候去做别的事"来实现高效的并发。这里的关键词是"并发"而不是"并行"——厨师始终只有一个人,但他通过合理安排时间,让多道菜同时推进。

await 就是厨师说"这一步我要等,先去做别的"的那个动作。如果一段代码没有 await,就相当于厨师从头到尾盯着一锅汤直到烧开——别的菜全部停摆。

记住这个画面,下面所有概念都会变得直觉化。


第一部分:迭代体系——“怎样一个一个地拿到值”

为什么需要迭代协议

假设你有一百万条数据需要处理。最暴力的方式是把它们全部加载到一个列表里,但这会吃掉大量内存。更聪明的方式是"要一条取一条"——你不需要同时持有全部数据,只需要一个机制能让你说"给我下一个"。

这就是迭代协议存在的原因。它和 Java 的 Iterator 接口本质上解决的是同一个问题,只不过 Python 的实现更轻量,语法糖更多。

三个核心角色

1️⃣ Iterable(可迭代对象) 是最宽泛的概念,它的意思就是"这个东西可以被遍历"。在 Python 里,一个对象只要实现了 __iter__() 方法,它就是 Iterable。listtupledictstrset 全都是。类比 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)

这里:

  • lstIterable
  • it1it2 是两个不同的 Iterator

它们各自维护自己的遍历位置:

next(it1)  # 10
next(it1)  # 20

next(it2)  # 10

你会发现:

  • it1 已经走到第二个元素了
  • it2 还是从头开始

这就像“同一本书,可以拿出两个书签分别夹在不同页”:

  • lst 是那本书本身
  • it1it2 是两个不同书签位置

所以 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 是一种 IteratorIterator 是一种 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)就是那个厨师。它的工作方式是一个无限循环:

  1. 从就绪队列里取出一个协程
  2. 执行这个协程,直到它遇到 await 或者执行完毕
  3. 如果遇到 await,把协程挂起,记录它在等什么
  4. 检查有没有之前挂起的协程,它等的东西已经好了
  5. 如果有,恢复那个协程
  6. 回到第 1 步

整个过程在单线程里完成。这意味着任何一个协程如果不 await,就会独占这个线程,所有其他协程都得等着。这也是为什么在 async def 里写 time.sleep() 是灾难——它直接冻结了唯一的厨师。


第三部分:同步与异步的对应关系——一张完整的映射表

理解了前两部分之后,现在可以建立一张完整的对应关系了。Python 的迭代体系其实是两套平行结构:同步体系和异步体系。它们的概念一一对应,只是异步版本在每个"取值"操作上都加了"让出控制权"的能力。

函数的四种组合,这是最核心的区分:def + return 是普通同步函数,调用后直接得到返回值。def + yield 是同步生成器,调用后得到一个可以用 for 遍历的生成器对象。async def + return 是协程函数,调用后得到协程对象,必须用 await 来获取结果。async def + yield 是异步生成器,调用后得到一个可以用 async for 遍历的异步生成器对象。

这四个组合覆盖了你在实际代码中会遇到的所有情况。特别注意:yieldasync 是两个独立的维度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 负责调度协议,线程池负责隔离阻塞代码。两者是协作关系,不是替代关系。 异步框架不能消灭阻塞——它只能把阻塞隔离到不会影响事件循环的地方。


第五部分:八条核心规则(速查用)

  1. yield 只说明"分步产出值",和异步没有任何关系。 def + yield 是同步生成器,async def + yield 才是异步生成器。
  2. async def 只说明"这个函数遵循异步协议",不保证内部代码不阻塞。async def 里写 time.sleep() 照样会冻结事件循环。
  3. 真正让协程让出执行权的是 await 没有 awaitasync def 和普通函数在执行效果上没有区别。
  4. await 只能等待可等待对象。 最常见的是协程对象、TaskFuture。底层靠 __await__() 协议。
  5. for 对应同步迭代,async for 对应异步迭代。 两者结构对称,不要混用。
  6. 事件循环是单线程的。 任何在事件循环线程里的阻塞操作都会卡住所有协程。
  7. 同步阻塞代码需要通过线程池桥接进异步系统。 asyncio.to_thread()anyio.to_thread.run_sync() 是标准做法。
  8. Generator 是 Iterator 的特例,Iterator 是 Iterable 的特例。 同样地,AsyncGeneratorAsyncIterator 的特例,AsyncIteratorAsyncIterable 的特例。