内容提要
PEP 789 提出了一个新的上下文管理器 sys.prevent_yields(),旨在解决 Python 中异步生成器的任务取消问题。该管理器在其上下文中禁止使用 yield,以避免错误的任务取消和异常处理。提案强调结构化并发的重要性,并建议在 asyncio 等库中实施,以确保任务的正确管理和异常的准确传递。
延伸解读
问题根源:yield 与结构化并发的冲突
PEP 789 指出,在取消作用域(如 asyncio.TaskGroup 或 timeout)内使用 yield 会挂起当前帧,导致子任务在父帧挂起时继续运行。这违反了结构化并发中子任务应封装在父帧内的原则,可能引发错误的任务取消、异常传递错位或丢失。文章通过多个示例展示了这些 bug 的实际表现,强调这是生成器控制流与结构化并发控制流的根本不兼容。
解决方案:sys.prevent_yields 上下文管理器
提案引入 sys.prevent_yields() 上下文管理器,在其作用域内尝试 yield 会引发 RuntimeError。该管理器将由 asyncio 等库用于实现 TaskGroup、timeout 和取消操作,从而防止在取消作用域内挂起帧。同时,为 @contextmanager 等装饰器提供机制,允许在装饰函数内安全地 yield,因为此时异常会被重定向到正确的任务。
兼容性与迁移路径
新增 sys.prevent_yields 及对 @contextmanager 的修改是向后兼容的,但在 asyncio.TaskGroup 等标准库中禁止 yield 会破坏现有代码。提案建议分阶段弃用:3.14 在 asyncio 调试模式下发出 DeprecationWarning,3.15 默认警告并在调试模式下报错,3.16 转为硬错误。下游框架可立即采用此功能,而标准库的迁移需社区反馈。
替代方案与局限
文章讨论了多种替代方案,如 PEP 533 的确定性清理、完全弃用异步生成器、将异常传递到正确位置、以及通过字节码检查实现。但这些方案要么无法解决 TaskGroup 的根本问题,要么带来性能开销或兼容性问题。PEP 789 选择解释器支持,以平衡正确性和性能,但承认无法覆盖所有 yield-within-with 问题(如 decimal.localcontext)。
Q&A
PEP 789 的主要目的是什么?
PEP 789 旨在通过引入新的上下文管理器 sys.prevent_yields() 来解决 Python 中异步生成器的任务取消问题。
sys.prevent_yields() 是如何工作的?
sys.prevent_yields() 在其上下文中禁止使用 yield,试图 yield 时会引发 RuntimeError,从而防止错误的任务取消和异常处理。
为什么在异步生成器中使用 yield 会导致问题?
在异步生成器中使用 yield 会导致任务取消和异常处理错误,违反了结构化并发的设计原则。
PEP 789 如何促进结构化并发?
PEP 789 强调结构化并发的重要性,建议在 asyncio 等库中实施,以确保任务的正确管理和异常的准确传递。
如何安全地在用户定义的上下文管理器中使用 yield?
可以通过将生成器实现为上下文管理器来安全地使用 yield,这样可以确保异常被正确传递到预期的任务。
PEP 789 对现有代码的兼容性如何?
PEP 789 的引入是向后兼容的,但禁止在 asyncio.TaskGroup 和 asyncio.timeout 中使用 yield 可能会影响一些现有代码。