Python 生成器被实现为暂停的帧对象 (PyFrameObject),在调用之间保持其执行状态。当调用 send(value) 时,CPython 的内部 gen_send_ex() 函数将该值推送到生成器的值栈中,然后 yield 表达式从该栈中弹出并返回给调用者。这与初始的 next() 调用不同,后者隐式地发送 None 以引导生成器从其初始状态 (f_lasti == -1) 到第一个 yield 表达式。如果在生成器首次 yield 之前以非 None 的值调用 send(),CPython 会引发 TypeError,因为生成器帧缺少接收该值的栈位置。这种架构区分确保双向通信只在生成器达到其第一个暂停点后开始。
我们需要实现一个考虑背压的数据管道,用于处理高频市场数据流,在这个管道中,下游消费者可以动态地向上游生产者发出信号,以节流或恢复数据流,而不丢失消息或耗尽内存。
一个考虑的方法是使用 threading 和有界的 queue.Queue 实例在管道各阶段之间进行通信。虽然这提供了熟悉的阻塞语义和线程安全,但它遭遇了严重的 GIL 争用和上下文切换开销,在高吞吐量下单纯协调就消耗了 15% 的 CPU,同时增加了不可预测的延迟峰值。
另一个替代方案涉及迁移到 asyncio 协程和 async/await 语法。这将消除 GIL 争用,但需要将我们的同步数值分析库完全重写为异步兼容的形式,从而创建一个病毒式重构,涉及数千行业务逻辑并引入与遗留 C 扩展的兼容性问题。
我们最终选择了一种基于生成器的协作多任务方法,使用 send() 向上游传递 "需求信用"。该解决方案完全避免了 GIL 开销,无需库重写,因为生成器可以在同步代码中工作,并通过 demand = (yield data_chunk) 模式提供明确的控制流,允许下游消费者立即通过发送零值来暂停上游生产。
结果是,与队列方法相比,内存使用减少了 40%,延迟稳定在 5 毫秒以下,并且代码库保持可读,明确的 yield 点标记暂停边界。
为什么在新创建的生成器上使用非 None 值调用 send() 会引发 TypeError,这种限制如何强制执行生成器协议?
当生成器首次创建时,其帧指针 f_lasti 为 -1,表示尚未执行任何字节码。当调用 send() 时,CPython 解释器会检查生成器是否未启动;如果发送的值不是 None,则引发 TypeError,因为尚未到达 yield 表达式以提供接收该值的栈槽。这种强制执行确保了生成器初始化逻辑在双向通信开始之前完成,从而保持值仅在明确的 yield 暂停点流入生成器。
generator.close() 如何确保生成器内的清理代码执行,且 GeneratorExit 异常与常规异常有何不同?
close() 方法通过调用 throw(GeneratorExit) 将 GeneratorExit 异常发送到生成器的当前暂停点。GeneratorExit 继承自 BaseException 而非 Exception,以防止其被可能不当捕获的通用 except Exception 处理程序捕获。如果生成器捕获 GeneratorExit 并重新引发它或正常退出,close() 将静默返回;然而,如果生成器对 GeneratorExit 产生响应并生成一个值,CPython 将引发 RuntimeError,因为正在关闭的生成器不得产生新值。该机制确保在强制终止期间,生成器体内的 finally 块和上下文管理器得以执行。
什么机制允许 yield from 在嵌套生成器之间透明地处理发送的值,这与使用循环和 send() 手动委托有何不同?
yield from 语法不仅委托迭代,而且将完整的生成器协议委托给子生成器。当外部生成器执行 yield from subgen() 时,CPython 将调用者的 send(value) 转换为直接发送到子生成器,直到它引发 StopIteration(其值成为 yield from 表达式的结果)。这与手动委托不同,在手动委托中,类似 for x in subgen(): yield x 的循环无法拦截发送到外部生成器的值以转发到内部生成器。因此,yield from 结构基本上将调用栈展平,实现通过任意深的生成器嵌套进行双向数据流,而无需样板转发代码,同时保持适当的异常传播和关闭语义。