PEP 789:通过限制异步生成器中的 yield 来防止任务取消错误

PEP 789:通过限制异步生成器中的 yield 来防止任务取消错误

💡 原文英文,约3600词,阅读约需13分钟。
📝

内容提要

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 可能会影响一些现有代码。

🏷️

标签

➡️

继续阅读