面试准备

最近改动

更新页面 Python 并发语法

页面 kb/python-concurrency · 修改(rev 14) · cc · 2026-10-25 23:21

body

@@ -547,12 +547,104 @@
 ### 协程是怎么实现的
 
-`asyncio` 的协程是三样东西拼起来的:Future、Task 和事件循环。下面先用一个小例子看协程怎么停下、怎么接着跑,再从零写一个 40 行的最小事件循环,逐步走一遍。看懂这一节,`await` 底下发生了什么就清楚了。
+`asyncio` 的协程是三样东西拼起来的:Future、Task 和事件循环。协程能「停下、过一会儿再接着跑」,靠的是 Python 的生成器。所以这一节先讲生成器,再看协程怎么停下、怎么接着跑,最后从零写一个 40 行的最小事件循环,逐步走一遍。看懂这一节,`await` 底下发生了什么就清楚了。
+
+#### 先认识生成器
+
+> [!term] 生成器(generator)
+> 调用一个函数体里写了 `yield` 的函数,会得到一个生成器对象,这时函数体一行都还没执行。这个对象能跑一段、停下、交出一个值,下次被要求时,再从停下的地方接着跑。普通函数一旦返回,局部变量就没了。生成器停下时,局部变量和跑到了哪一行,都留在生成器对象里。
+
+`yield` 就是生成器里「停在这里,并把后面这个值交给外面」的意思。外面用 `next(g)` 要下一个值,`g` 就从上次停下的地方接着跑,跑到下一个 `yield` 停下:
+
+```python
+def count_to_two():
+    print("  生成器里:开始")
+    yield 1
+    print("  生成器里:从第一个 yield 后面接着跑")
+    yield 2
+    print("  生成器里:跑到函数末尾")
+
+g = count_to_two()                 # 只建了生成器对象,函数体一行都没执行
+print("外面:建好了", type(g).__name__)
+print("外面:拿到", next(g))       # 跑到第一个 yield 停下,把 1 交出来
+print("外面:拿到", next(g))       # 从停下的地方接着跑,到第二个 yield 停下
+try:
+    next(g)                        # 接着跑到函数末尾,没有值可交了
+except StopIteration:
+    print("外面:收到 StopIteration,生成器跑完了")
+```
+
+实测输出:
+
+```
+外面:建好了 generator
+  生成器里:开始
+外面:拿到 1
+  生成器里:从第一个 yield 后面接着跑
+外面:拿到 2
+  生成器里:跑到函数末尾
+外面:收到 StopIteration,生成器跑完了
+```
+
+- `count_to_two()` 只建了生成器对象,函数体一行都没执行,所以「开始」排在「建好了」后面。
+- 第一次 `next(g)` 从函数开头跑,打印「开始」,到 `yield 1` 停下,把 1 交给外面。
+- 第二次 `next(g)` 从 `yield 1` 的下一行接着跑,打印一行,到 `yield 2` 停下,交出 2。
+- 第三次 `next(g)` 接着跑到函数末尾,已经没有值可交了,于是抛出 `StopIteration`。
+
+> [!term] StopIteration
+> Python 内置的一个异常,意思是「没有下一个值了」。生成器的函数体跑到末尾或者执行了 `return`,再对它调 `next()` 或 `send()`,就会抛出它。`return` 后面的值放在这个异常的 `value` 属性里。它是一个正常的结束信号,不是出错。
+
+`for` 循环背后就是这件事:反复调 `next`,收到 `StopIteration` 就结束循环。所以平时用 `for` 遍历生成器,从来看不到这个异常:
+
+```python
+for x in count_to_two():           # for 循环就是反复调 next,收到 StopIteration 就结束
+    print("for 拿到", x)
+```
+
+实测打印「for 拿到 1」「for 拿到 2」,中间夹着生成器里的那几行,然后循环自己结束了。
+
+外面除了用 `next` 要值,还能用 `send` 往生成器里传一个值进去。`yield` 本身也是一个表达式:它交出一个值,停下,等外面下次 `send(v)` 时,`v` 就成了这个 `yield` 表达式的值:
+
+```python
+def echo():
+    received = yield "第一次交出的值"
+    print("  生成器里:yield 表达式的值是", received)
+    return "函数的返回值"
+
+g = echo()
+print("外面:拿到", g.send(None))   # 第一次只能 send(None),效果和 next(g) 一样
+try:
+    g.send("你好")                  # "你好" 成了 yield 表达式的值,然后函数跑到 return
+except StopIteration as e:
+    print("外面:StopIteration,返回值在 e.value 里:", e.value)
+```
+
+实测:第一次拿到「第一次交出的值」,生成器里打印「yield 表达式的值是 你好」,最后外面收到 `StopIteration`,`e.value` 是「函数的返回值」。第一次只能 `send(None)`,因为生成器还没跑到任何 `yield`,没有地方接收传进来的值,这时 `send(None)` 和 `next(g)` 效果一样。
+
+协程的返回值也是这样交出来的:
+
+```python
+async def add_one():
+    return 41 + 1
+
+coro = add_one()
+try:
+    coro.send(None)                 # 没有 await,一次就跑到 return
+except StopIteration as e:
+    print("协程的返回值也在 StopIteration 里:", e.value)
+```
+
+实测打印「协程的返回值也在 StopIteration 里: 42」。`asyncio` 的 Task 就是靠接住这个异常,拿到协程的结果。
 
 #### 协程怎么停下、怎么接着跑
 
-> [!term] 生成器(generator)
-> 函数体里有 `yield` 的函数。调用它不执行函数体,只得到一个生成器对象。每次调它的 `send()`,它就从上次停下的地方接着跑,跑到下一个 `yield` 停下,`yield` 出来的值就是 `send()` 的返回值。函数跑完时,`send()` 抛出 `StopIteration`。
-
-协程对象也有 `send()`,用法和生成器一样。`await` 一个对象时,Python 会调用这个对象的 `__await__()`,它通常就是一个生成器,里面的 `yield` 就是协程的暂停点:
+协程对象也有 `send()`,用法和生成器一样。区别是协程的函数体里不写 `yield`,它在 `await` 处停下。`await` 后面能写什么,有一个专门的说法:
+
+> [!term] 可等待对象(awaitable)
+> 能写在 `await` 后面的对象:协程、`asyncio` 的 Task 和 Future,或者任何定义了 `__await__` 方法的对象。`__await__` 要返回一个迭代器,通常就是一个生成器。
+
+> [!term] 挂起点(suspension point)
+> 协程执行到这里会停下、把控制权交给外面的那个位置。在 Python 里,`await x` 会调用 `x.__await__()`,协程就停在这个生成器里的 `yield` 上。`yield` 出来的值穿过协程,成为外面那次 `send()` 的返回值。外面下次再 `send()`,协程就从这个 `yield` 的下一行接着跑。
+
+下面的 `Box` 就是一个最小的可等待对象(实测 `isinstance(Box(), collections.abc.Awaitable)` 是 True,`Box().__await__()` 是一个生成器):
 
 ```python
@@ -562,5 +654,5 @@
     def __await__(self):
         if not self.done:
-            yield self                   # 暂停点:把自己交给外面调 send 的人
+            yield self                   # 挂起点:协程停在这里,self 交给外面调 send 的人
         return self.value                # 恢复以后,这就是 await 表达式的值