Skip to main content

Chapter 67: genobject.c and frameobject.c

生成器和协程让 Python 函数具备“暂停后继续”的执行形态。读完本章后,读者应能追踪一个生成器或协程从创建、首次恢复、暂停、再次恢复、返回、关闭到异常传播的 runtime 路径,并能判断局部变量、frame、异常状态和 asyncio.Task 之间的持有关系。

本章的主线是一个短生成器和一个短协程。生成器 pipeline()yield 处暂停,协程 worker()await 处暂停。二者在 Python 语法层看起来属于不同抽象,在 CPython runtime 层共享一组核心问题:谁保存暂停点,谁保存 locals,谁负责下一次 resume,返回值和异常沿哪条路径传播。

CPython 的源码阅读入口主要是 Objects/genobject.cObjects/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。函数体内的 sourcecursoritemreceived 会在执行推进后进入该生成器持有的 frame。worker(fut) 的函数调用会得到一个 coroutine object。它等待的 fut、恢复后的 value 和返回值会通过 coroutine frame 与 Task 状态传播。后文每一节都会回到这段材料,把可见行为映射到 genobject.cframeobject.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 留在暂停状态。此时 sourcecursoritem 属于这个暂停 frame 的 live locals,inspect.getgeneratorlocals(g) 可以读取它们的当前值。Python 3.14 的 inspect 文档把生成器状态分为 GEN_CREATEDGEN_RUNNINGGEN_SUSPENDEDGEN_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 fromawait 代理对象上的情况,并在需要时清理内嵌 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_framecoro.cr_runningcoro.cr_await 一类属性观察协程状态;inspect.getcoroutinestate() 会报告 CORO_CREATEDCORO_RUNNINGCORO_SUSPENDEDCORO_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_framecr_frameag_frame 或 traceback 中的 frame;对 CPython 执行器来说,它通常是内部 _PyInterpreterFrame,必要时再形成 Python 可见的 PyFrameObject

下面的状态图只描述本章关心的 generator/coroutine frame 生命周期。它忽略字节码优化细节,突出 frame 状态和持有者变化。

Created 阶段已经有可恢复对象,函数体的第一条用户语句还没有产生运行效果。Executing 阶段由解释器循环持有当前 frame,f_lasti 或内部 instruction pointer 随字节码推进。Suspended 阶段保留下一次恢复所需的执行位置和对象栈。Cleared 阶段表示该 frame 的可执行状态已经释放,Python 层的 gi_framecr_frame 可能变为 None,也可能仍有 traceback 或外部引用持有已经结束的 frame object。

暂停点的关键不是“保存一行代码位置”这么简单。yield 处需要保存局部变量、循环状态、异常处理上下文和 value stack。yield fromawait 还需要记录当前正在等待的子 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.TaskFuture 和 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 绑定为 41return 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,因此可以看到 datagen.send("again")"again" 作为第一个 yield 表达式的结果送回 frame,token 绑定为 "again",随后生成器推进到第二个 yield 并产出 20gen.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 求值得到 6value 绑定为 6return value + 1 让 Task 进入 done 状态。await task 读取 Task 的完成值,result7。若 add_one() 内部抛出异常,异常会保存到 Task,并在 await tasktask.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,再看等待对象,最后看恢复者和完成状态。