Chapter 67: genobject.c and frameobject.c
生成器和协程让 Python 函数具备“暂停后继续”的执行形态。读完本章后,读者应能追踪一个生成器或协程从创建、首次恢复、暂停、再次恢复、返回、关闭到异常传播的 runtime 路径,并能判断局部变量、frame、异常状态和 asyncio.Task 之间的持有关系。
本章的主线是一个短生成器和一个短协程。生成器 pipeline() 在 yield 处暂停,协程 worker() 在 await 处暂停。二者在 Python 语法层看起来属于不同抽象,在 CPython runtime 层共享一组核心问题:谁保存暂停点,谁保存 locals,谁负责下一次 resume,返回值和异常沿哪条路径传播。
CPython 的源码阅读入口主要是 Objects/genobject.c、Objects/frameobject.c 和内部 frame 定义所在的 Include/internal/pycore_frame.h。这些文件中的字段名和辅助函数具有版本敏感性;本章使用 CPython 3.14 文档和 CPython main 分支源码形状建立阅读模型,具体本地版本应以 CPYTHON_SRC=/path/to/cpython 中的源码为准。
先固定贯穿材料:
import asyncio
import inspect
def pipeline():
source = ["A", "B"]
cursor = 0
try:
for item in source:
cursor += 1
received = yield (cursor, item)
if received == "stop":
return "stopped"
finally:
source.clear()
async def worker(fut):
value = await fut
return value + 1
pipeline() 的函数调用会得到一个 generator object。函数体内的 source、cursor、item 和 received 会在执行推进后进入该生成器持有的 frame。worker(fut) 的函数调用会得到一个 coroutine object。它等待的 fut、恢复后的 value 和返回值会通过 coroutine frame 与 Task 状态传播。后文每一节都会回到这段材料,把可见行为映射到 genobject.c 和 frameobject.c 的责任边界。
67.1 generator object
generator object 是“可恢复函数调用”的持有者。普通函数调用会执行函数体并在返回后释放当前调用 frame;生成器函数调用先创建一个对象,把 code、frame 状态、名字、异常状态和弱引用等信息放到该对象上,随后由 next()、send()、throw() 或 close() 推进。
在 CPython 源码层,genobject.c 负责 generator、coroutine 和 async generator 三类对象的大部分通用逻辑。现代 CPython 使用内部 _PyInterpreterFrame 表达正在解释器中执行或等待恢复的 frame;生成器对象内部持有类似 gi_iframe 的内嵌 frame,并用 gi_frame_state 一类状态字段表达 created、executing、suspended、cleared 等阶段。Python 层看到的 g.gi_frame 是面向 introspection 的 frame 视图,内部执行使用的 frame 表示会随版本演进。
对贯穿材料执行下面的片段,可以观察到 generator object 的状态推进:
g = pipeline()
print(inspect.getgeneratorstate(g))
print(next(g))
print(inspect.getgeneratorstate(g))
print(inspect.getgeneratorlocals(g))
第一次调用 pipeline() 时,函数体尚未推进到 source = ["A", "B"]。此时 generator object 已经存在,code object 和初始 frame 已经准备好,inspect.getgeneratorstate(g) 会报告等待启动的状态。next(g) 进入 resume 路径,解释器执行到第一个 yield,把 (1, "A") 作为本轮产出返回给调用方,并把 frame 留在暂停状态。此时 source、cursor 和 item 属于这个暂停 frame 的 live locals,inspect.getgeneratorlocals(g) 可以读取它们的当前值。Python 3.14 的 inspect 文档把生成器状态分为 GEN_CREATED、GEN_RUNNING、GEN_SUSPENDED 和 GEN_CLOSED,这组名字适合从 Python 层验证状态变化。
send() 的核心差异在于恢复时向 frame 的 value stack 推入一个对象。对贯穿材料执行 g.send("stop") 时,前一次 yield 表达式的结果会成为局部变量 received,随后 return "stopped" 结束生成器。Python 层表现为抛出 StopIteration,其 value 保存生成器的返回值;runtime 层表现为 genobject.c 区分 yield 和 return 两类离开解释器循环的结果。公开源码中 gen_send_ex 会先检查 frame state,再把对象设为 executing,随后进入执行 frame 的路径;gen_send_ex2 会把参数压入 frame stack,调用解释器执行 frame,并根据本轮是 yield 还是 return 决定返回给调用方的结果形态。
generator object 同时承担 reentrancy 保护。一个生成器处在 executing 状态时,再次对同一对象执行 send() 或 next() 会触发“already executing”类错误。这个检查属于对象状态检查,位置早于解释器继续执行 frame。它保证同一个暂停 frame 在同一时间只有一个恢复者,局部变量、value stack 和异常状态可以按单线程式执行顺序更新。
关闭路径把生成器的生命周期和资源释放连接起来。close() 会向暂停的生成器注入 GeneratorExit,让 finally 块得到执行机会。贯穿材料中的 source.clear() 位于 finally 中,因此在 g.close() 或生成器对象释放时可以收束资源状态。genobject.c 中的 close/finalize/dealloc 逻辑会处理未启动、已结束、暂停在 yield from 或 await 代理对象上的情况,并在需要时清理内嵌 frame 和异常状态。
阅读 generator object 的可复用顺序是:先确认 Python 层动作是创建、next()、send()、throw() 还是 close();再确认对象状态是 created、executing、suspended 还是 closed;接着查看恢复时进入 frame 的输入值或异常;最后判断本轮离开 frame 的原因是 yield、return、异常还是关闭。这个顺序可以解释大部分生成器行为,包括首次 send() 只能传入 None、暂停处 locals 可见、返回值进入 StopIteration.value、关闭时执行 finally。
67.2 coroutine object
coroutine object 是带有 awaitable 语义的可恢复函数调用。async def 函数被调用时返回 coroutine object,函数体也等待外部恢复才执行。它和 generator object 共享暂停 frame、running state、异常状态和 resume 路径;它额外承载 await 语义、未 await 警告、已完成协程复用错误和 Task 调度关系。
对贯穿材料执行下面的片段,可以看到 coroutine object 创建后先处于等待启动状态:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
fut = loop.create_future()
coro = worker(fut)
print(inspect.getcoroutinestate(coro))
print(coro.cr_frame is not None)
worker(fut) 返回的 coro 持有等待执行的 frame。fut 是调用参数,会进入该 frame 的 fast locals。Python 层可以通过 coro.cr_frame、coro.cr_running、coro.cr_await 一类属性观察协程状态;inspect.getcoroutinestate() 会报告 CORO_CREATED、CORO_RUNNING、CORO_SUSPENDED 或 CORO_CLOSED。这些状态名字来自 Python 文档,适合做现象确认;CPython 内部状态字段和辅助宏应按具体源码版本读取。
协程第一次运行到 await fut 时,会把控制权交回调度者。await 的 runtime 含义是当前 coroutine frame 暂停,并把等待对象暴露给上层驱动者。上层可以是另一个协程,也可以是 asyncio.Task。当 fut 完成后,驱动者把结果送回 worker() 的暂停点,await fut 表达式求值得到 fut 的结果,随后 value + 1 成为协程的最终返回值。
coroutine object 和 generator object 的工程边界体现在调用接口和错误形态上。generator 是 iterator,可以用 next() 推进;native coroutine 通过 awaitable 协议被驱动,通常交给 Task 或事件循环。一个 coroutine 完成后再次 await 会触发复用错误。一个 coroutine 对象在释放时仍处于 created 状态,CPython 会发出未 await 的运行时警告。源码层可以在 genobject.c 的 finalize 路径看到 coroutine code flag 与警告逻辑的连接。
协程的返回值也沿暂停机制传播。普通 return value 在 coroutine 里不会变成调用者直接拿到的同步返回值;它会成为 await 表达式或 Task result 的完成值。异常也走同一条传播线:协程内部抛出的异常会沿 await 链传给上层,若由 Task 包装,则进入 Task 的 exception 状态并在 task.result() 处重新抛出。
阅读 coroutine object 的顺序可以直接复用 generator 的前半段,再增加 awaitable 边界检查:先确认 async def 调用创建了 coroutine object;再确认当前状态和 cr_frame 是否仍然存在;随后检查 cr_await 指向的等待对象;最后看上层驱动者如何把结果、异常或取消重新送回 coroutine frame。这个顺序能解释“协程创建后没有执行”“await 后 locals 仍被保存”“Task 完成后 result 可取”“未 await 协程释放时出现警告”等行为。
67.3 suspended frame
suspended frame 是生成器或协程暂停后仍然可恢复的执行记录。它保存 code object、指令位置、locals、value stack、异常处理状态以及与上一层 frame 的连接信息。对 Python 使用者来说,它对应 gi_frame、cr_frame、ag_frame 或 traceback 中的 frame;对 CPython 执行器来说,它通常是内部 _PyInterpreterFrame,必要时再形成 Python 可见的 PyFrameObject。
下面的状态图只描述本章关心的 generator/coroutine frame 生命周期。它忽略字节码优化细节,突出 frame 状态和持有者变化。
Created 阶段已经有可恢复对象,函数体的第一条用户语句还没有产生运行效果。Executing 阶段由解释器循环持有当前 frame,f_lasti 或内部 instruction pointer 随字节码推进。Suspended 阶段保留下一次恢复所需的执行位置和对象栈。Cleared 阶段表示该 frame 的可执行状态已经释放,Python 层的 gi_frame 或 cr_frame 可能变为 None,也可能仍有 traceback 或外部引用持有已经结束的 frame object。
暂停点的关键不是“保存一行代码位置”这么简单。yield 处需要保存局部变量、循环状态、异常处理上下文和 value stack。yield from 和 await 还需要记录当前正在等待的子 iterator 或 awaitable。公开源码中的 genobject.c 会在查询 gi_yieldfrom 之类属性时从 frame stack 中取出当前委托对象;这说明暂停 frame 保留的不只是源码行号,还包括继续执行所需的栈内对象。
frameobject.c 负责把内部 frame 暴露成 Python 层可读写或可检查的对象。Python 3.13 之后,优化 frame 的 f_locals 行为已经向 write-through proxy 方向演进;CPython main 分支的 frameobject.c 中可以看到 FrameLocalsProxy 相关实现,用来把 frame 的 fast locals 以 mapping 形式暴露。这个实现细节说明一件事:locals()、frame.f_locals 和内部 fast locals 属于不同层级,读取 frame locals 时需要确认当前 Python 版本和代码对象是否使用 optimized locals。
对贯穿材料执行下面的片段,可以看到暂停 frame 的 locals 来自真实执行状态:
g = pipeline()
first = next(g)
frame = g.gi_frame
print(first)
print(frame.f_code.co_name)
print(frame.f_locals["cursor"])
print(frame.f_lasti >= 0)
frame.f_code.co_name 指向 pipeline 的 code object,frame.f_locals["cursor"] 反映暂停时的局部变量值,frame.f_lasti 或相近的指令位置属性反映解释器已经推进过的指令范围。不同 CPython 版本对指令偏移、异常表和调试位置信息的展示会变化,因此源码阅读时应把它当成定位线索,而非跨版本稳定数值。
判断一个 frame 是否仍可恢复,应按三步检查。第一步看持有者:它由 generator、coroutine、async generator、当前调用栈、traceback 还是外部变量持有。第二步看执行状态:created、executing、suspended、closed 中哪一个符合当前现象。第三步看恢复入口:下一次输入来自 next()、send(value)、throw(exc)、Task 完成值还是取消异常。这个检查顺序比单看 gi_frame is None 更稳定,因为 introspection 属性只是内部状态的外部投影。
67.4 frame persistence
frame persistence 指 frame 因生成器、协程、traceback、inspect 或外部引用而延长生命周期。Python 的局部变量存放在 frame 的 locals 区域;只要 frame 仍被持有,locals 中引用的对象就会继续存活。生成器暂停、协程等待、异常 traceback 和调试工具持有 frame,都会让局部对象的生命周期超过一次普通函数调用。
贯穿材料中的 source 是最小例子。next(g) 后,pipeline() 已经把 source 绑定到列表对象,并暂停在 yield。此时调用方虽然没有直接拿到 source 名字,列表仍然由生成器 frame 的 locals 持有。只要 g 存活且 frame 处于 suspended 状态,source 就会随 frame 一起存活。g.close() 触发 finally 后,source.clear() 执行,随后 frame 进入清理路径,locals 中的引用被释放。
下面的代码把这个生命周期做得更明显:
class Payload:
def __init__(self, name):
self.name = name
def __del__(self):
print("release", self.name)
def holder():
payload = Payload("frame-local")
yield "paused"
return payload.name
g = holder()
print(next(g))
print(inspect.getgeneratorlocals(g)["payload"].name)
g.close()
Payload("frame-local") 绑定到 payload 后,暂停 frame 持有这个对象。inspect.getgeneratorlocals(g) 只是读取当前 frame locals;真正让对象存活的是 frame 的 locals 引用。g.close() 进入关闭路径后,生成器完成清理,payload 的引用随 frame 清理而释放。CPython 的引用计数通常会让释放时机显得很及时;其它 Python 实现可以选择不同 GC 策略,因此这里的稳定结论应写成“frame 持有 locals 会延长对象可达性”。
traceback 会把 frame persistence 推到异常场景。异常沿调用栈传播时,traceback 链保存出错路径上的 frame。只要异常对象或 traceback 对象仍被持有,对应 frame 和 locals 就可能继续可达。这个特性对调试有价值,因为它保留了错误现场;它也会影响内存排查,因为大型局部对象可能被 traceback 间接持有。工程中需要把异常记录、日志聚合、调试缓存和 frame 引用纳入同一张引用图。
inspect 也能把临时 frame 变成持久对象。调用 inspect.currentframe()、读取 gi_frame、保存 traceback 或把 frame 放进全局列表,都会增加一条从用户对象到 frame 的引用路径。frameobject.c 提供的 frame API 让调试器、profiler、coverage 工具和交互式环境能够读取执行状态;代价是 frame 的生命周期可能被工具延长。工具代码完成检查后,应释放不再使用的 frame 引用;需要切断已结束 frame 的 locals 时,可以在受控场景使用 frame.clear(),同时确认该 frame 不处于执行中。
判断 frame persistence 的顺序是:先画出引用入口,确认 generator/coroutine/traceback/inspect 哪个对象持有 frame;再查看 frame locals 中是否有大对象、锁、文件句柄、网络连接或闭包引用;随后确认清理触发点是 return、异常传播、close()、Task cancellation 还是外部引用释放;最后根据 CPython 引用计数和循环 GC 边界判断可观察释放时机。这个顺序能把“生成器暂停导致内存占用上涨”“异常日志后对象迟迟仍可达”“调试器保存 frame 后局部状态持续存在”放到同一套模型下解释。
67.5 async runtime relation
async runtime relation 指 coroutine object、asyncio.Task、Future 和 event loop 之间的恢复关系。coroutine object 保存可恢复执行状态;Task 决定何时恢复它;Future 表示当前等待的异步结果;event loop 在 ready queue、I/O 回调和定时器之间调度下一次推进。Python 3.14 的 asyncio 文档说明,Task 用来在事件循环中运行 coroutine;当 coroutine 等待 Future 时,Task 暂停该 coroutine,Future 完成后再恢复它。
把贯穿材料交给 Task 后,关系会变成下面这样:
async def main():
loop = asyncio.get_running_loop()
fut = loop.create_future()
task = asyncio.create_task(worker(fut))
print(task.done())
fut.set_result(41)
result = await task
print(result)
asyncio.run(main())
asyncio.create_task(worker(fut)) 包装 coroutine object,并安排它在事件循环中尽快运行。worker() 第一次运行到 await fut 时暂停,Task 记录当前等待的 Future,并把执行权还给 event loop。fut.set_result(41) 让 Future 进入完成状态,event loop 安排 Task 继续推进 coroutine。Task 把 41 送回 await fut 的暂停点,value 绑定为 41,return value + 1 让 Task 进入 done 状态,await task 得到 42。
下面的时序图只描述结果传播,不展开 selector、I/O callback 和 ready queue 的细节。
异常传播沿相同通道运行。worker() 内部抛出异常时,Task 把异常保存为自身完成状态的一部分;调用 task.result() 会重新抛出该异常。取消也走恢复通道:task.cancel() 请求 Task 在下一次推进时向 coroutine 注入 CancelledError。如果 coroutine 在 await fut 处等待,Task 会把取消传播给等待的 Future,并在 coroutine 恢复时让它有机会执行 finally 或清理逻辑。协程吞掉取消异常会改变 Task 的最终状态;协程让取消异常继续传播时,Task 进入 cancelled 状态。
这一层关系解释了协程和生成器的主要工程差异。生成器通常由调用方显式 next() 或 send();协程通常由 Task 作为驱动者。生成器暂停后,调用方手动决定恢复时机;协程暂停后,恢复时机通常由 event loop 中的 Future 完成、定时器到期、I/O 就绪或取消请求决定。二者都依赖 suspended frame 保存执行现场,差异落在恢复者和完成状态的组织方式上。
定位 async 问题时,可以按四层检查。第一层看 coroutine object:它处于 created、running、suspended 还是 closed,cr_await 指向哪个等待对象。第二层看 Task:它是 pending、done、cancelled,还是持有异常。第三层看 Future:它是否已经 set_result、set_exception 或 cancel。第四层看 event loop:相关 callback 是否已经进入 ready queue,当前线程是否正在运行该 loop。这个顺序能把“await 卡住”“取消没有立即表现出来”“任务完成但异常在 result 处才出现”“协程对象创建后从未执行”等现象拆成可检查的 runtime 状态。
最小自检任务
阅读下面的代码,判断每一步之后生成器、协程、frame 和 Task 的状态关系。要求说明哪些局部变量仍被 frame 持有,哪个对象负责下一次 resume,最终返回值或异常会从哪里被观察到。
import asyncio
import inspect
def numbers():
data = [10, 20]
try:
token = yield data[0]
if token == "again":
yield data[1]
return "done"
finally:
data.clear()
async def add_one(fut):
value = await fut
return value + 1
async def main():
gen = numbers()
first = next(gen)
before = inspect.getgeneratorlocals(gen)
gen.send("again")
gen.close()
loop = asyncio.get_running_loop()
fut = loop.create_future()
task = asyncio.create_task(add_one(fut))
await asyncio.sleep(0)
fut.set_result(6)
result = await task
return first, before, result
答案要点
gen = numbers() 只创建 generator object,函数体内的 data 尚未绑定。next(gen) 推进到第一个 yield,返回 10,生成器进入 suspended 状态,frame locals 中持有 data = [10, 20]。before = inspect.getgeneratorlocals(gen) 读取暂停 frame 的 locals,因此可以看到 data。gen.send("again") 把 "again" 作为第一个 yield 表达式的结果送回 frame,token 绑定为 "again",随后生成器推进到第二个 yield 并产出 20。gen.close() 向暂停生成器注入关闭信号,finally 中的 data.clear() 执行,frame 进入清理路径,data 对列表的持有关系结束。
add_one(fut) 被 create_task() 包装后,Task 成为该 coroutine 的驱动者。await asyncio.sleep(0) 给事件循环一次运行机会,Task 推进 coroutine 到 await fut,coroutine frame 进入 suspended 状态,局部变量 fut 仍由 frame 持有。fut.set_result(6) 完成 Future,event loop 安排 Task 再次 resume coroutine,await fut 求值得到 6,value 绑定为 6,return value + 1 让 Task 进入 done 状态。await task 读取 Task 的完成值,result 为 7。若 add_one() 内部抛出异常,异常会保存到 Task,并在 await task 或 task.result() 处重新表现出来。
本章知识点总结
- 生成器对象:generator object 持有可恢复函数调用的 code、frame、状态和异常信息。
- 创建阶段:生成器函数调用先创建对象,函数体第一条用户语句等待外部 resume 才执行。
- 恢复入口:
next()、send()、throw()和close()都会先检查 frame state,再决定输入值或异常如何进入 frame。 - yield 结果:
yield会把值返回调用方,并把 frame 留在 suspended 状态以便后续恢复。 - return 结果:生成器中的
return value会结束 frame,并通过StopIteration.value表达返回值。 - 协程对象:coroutine object 复用暂停 frame 机制,并增加 awaitable 语义、未 await 警告和 Task 调度关系。
- 暂停 frame:suspended frame 保存 instruction pointer、locals、value stack 和异常处理状态。
- frame 视图:
frameobject.c把内部 frame 暴露为 Python 可检查对象,并负责f_locals等 introspection 行为。 - locals 生命周期:frame 被生成器、协程、traceback 或 inspect 持有时,locals 中引用的对象会继续可达。
- 关闭清理:
close()、返回、异常和取消都会进入 frame 清理路径,并给finally代码执行机会。 - Task 驱动:
asyncio.Task负责在 Future 完成、取消或事件循环调度时恢复 coroutine frame。 - Future 传播:Future 的 result、exception 或 cancellation 会沿 Task resume 路径进入
await暂停点。 - 排查顺序:先看 coroutine/generator 状态,再看 frame 和 locals,再看等待对象,最后看恢复者和完成状态。