ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入 CPython:修复 asyncio.Task 在 eager_start=True 且未显式传入 loop 时抛出 AttributeError(gh-issue-154695)

2026/9/11 20:49:18 拓冰建站 浏览量
深入 CPython:修复 asyncio.Task 在 eager_start=True 且未显式传入 loop 时抛出 AttributeError(gh-issue-154695) 深入 CPython修复 asyncio.Task 在 eager_startTrue 且未显式传入 loop 时抛出 AttributeErrorgh-issue-154695【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读本文基于 CPython 仓库中的一条修复记录Misc/NEWS.d/next/Core_and_Builtins/2026-07-25-17-35-14.gh-issue-154695.aQWLLv.rst剖析asyncio.Task在eager_startTrue且未显式传入loop参数时抛出AttributeError的根因与修复方案。读完本文你将理解 asyncio 急切启动eager start任务机制的完整调用链、事件循环参数在Future/Task构造过程中的解析时机以及如何在自定义Task子类与任务工厂场景下规避同类问题。一、修复记录原文与问题概述该 NEWS 条目归属于Core_and_Builtins对应 gh-issue-154695全文如下Fixasyncio.TaskraisingAttributeErrorwhen created witheager_startTrueand no explicitloopargument.翻译过来即当以eager_startTrue创建asyncio.Task、并且没有显式传入loop参数时任务会抛出AttributeError本次提交修复了该问题。这是一个典型的事件循环参数解析loop resolution缺陷Task.__init__的loop参数是可选的默认None但在eager_start这条代码路径上未解析的loopNone被直接当作事件循环对象使用导致对None调用方法时触发AttributeError。二、背景asyncio 的 eager_start 急切启动机制2.1Task.__init__的完整签名在 Lib/asyncio/tasks.py 中Task的构造器定义如下def __init__(self, coro, *, loopNone, nameNone, contextNone, eager_startFalse): super().__init__(looploop) # ... 类型检查、命名、context 拷贝等 ... if eager_start and self._loop.is_running(): self.__eager_start() else: self._loop.call_soon(self.__step, contextself._context) _py_register_task(self)其中四个关键字参数的含义为参数默认值作用loopNone任务绑定的事件循环为None时由Future基类解析nameNone任务名称缺省时自动生成为Task-1、Task-2等contextNone任务运行的contextvars上下文缺省时拷贝当前上下文eager_startFalse是否让任务立即启动执行而不是先调度到事件循环2.2 急切启动与普通调度的区别eager_startFalse默认行为时任务并不会立即运行而是通过self._loop.call_soon(self.__step, ...)把第一步执行调度到事件循环的下一个迭代随后通过_py_register_task登记到_scheduled_tasks一个weakref.WeakSet。eager_startTrue时只要当前循环正在运行self._loop.is_running()为真就会直接调用self.__eager_start()在当前栈上同步执行协程的第一步从而省去一次事件循环调度的往返延迟。__eager_start的实现如下def __eager_start(self): prev_task _py_swap_current_task(self._loop, self) try: _py_register_eager_task(self) try: self._context.run(self.__step_run_and_handle_result, None) finally: _py_unregister_eager_task(self) finally: try: curtask _py_swap_current_task(self._loop, prev_task) assert curtask is self finally: if self.done(): self._coro None self None # Needed to break cycles when an exception occurs. else: _py_register_task(self)可以看到急切启动期间任务被登记到_eager_tasks一个普通的set见 Lib/asyncio/tasks.py 的注释急切任务使用更快的普通集合一旦阻塞在 I/O 上再升级为常规任务。若协程第一步就完成任务立即done()并释放协程引用否则降级为常规调度任务并登记到_scheduled_tasks。2.3 相关配套 API任务工厂eager_start通常不是手工传给Task的而是通过任务工厂task factory全局开启。仓库中提供了两个配套 APILib/asyncio/tasks.pydef create_eager_task_factory(custom_task_constructor): Create a function suitable for use as a task factory on an event-loop. Example usage: loop.set_task_factory( asyncio.create_eager_task_factory(my_task_constructor)) ... def factory(loop, coro, *, eager_startTrue, **kwargs): return custom_task_constructor( coro, looploop, eager_starteager_start, **kwargs) return factory eager_task_factory create_eager_task_factory(Task)工厂内部会自动把loop显式传给任务构造器因此通过工厂创建的任务并不会触发本次 bug问题只出现在直接实例化Task或自定义Task子类且不传loop的场景。启用方式为loop.set_task_factory(asyncio.eager_task_factory)之后loop.create_task(...)创建的所有任务都会以eager_startTrue方式启动。BaseEventLoop.set_task_factory要求工厂可调用且签名为(loop, coro, **kwargs)get_task_factory()可查询当前工厂。三、Bug 场景与复现3.1 为什么正常路径不会触发常规创建任务的路径都会显式携带loopasyncio.create_task(coro, **kwargs)先调用events.get_running_loop()拿到当前运行循环再调用loop.create_task(...)Lib/asyncio/tasks.pyBaseEventLoop.create_task在有工厂时调用self._task_factory(self, coro, **kwargs)否则调用tasks.Task(coro, loopself, **kwargs)Lib/asyncio/base_events.py总是把loopself传下去。因此loop参数在这两条路径上永远不会是Noneeager_start分支能拿到真实循环对象。3.2 触发条件直接构造任务而不传loopimport asyncio async def asyncfn(): return 42 async def main(): t asyncio.Task(asyncfn(), eager_startTrue) # 未显式传 loop await t asyncio.run(main())在修复前这段代码会在eager_start相关分支对None调用事件循环方法如is_running()或call_soon()从而抛出AttributeError: NoneType object has no attribute ...。3.3 仓库中的回归测试本次修复在 Lib/test/test_asyncio/test_tasks.py 中新增了专门测试test_eager_start_true_no_loopdef test_eager_start_true_no_loop(self): # gh-154695: eager_start must use the resolved loop, not loopNone. async def asyncfn(): return 42 async def main(): t self.__class__.Task(asyncfn(), eager_startTrue) self.assertTrue(t.done()) self.assertEqual(await t, 42) asyncio.run(main(), loop_factoryasyncio.EventLoop)测试注释直接点明了修复原则eager_start must use the resolved loop, not loopNone急切启动必须使用解析后的循环而不是None。测试断言任务创建后立即done()且await结果为 42验证了急切启动路径在无显式loop时也能正常工作。值得注意两点测试通过self.__class__.Task间接引用任务类并配合asyncio.run(main(), loop_factoryasyncio.EventLoop)显式指定事件循环实现这意味着同一用例会覆盖 Python 实现与 C 加速实现Modules/_asynciomodule.c中的Task二者逻辑按注释约定保持同步见 Lib/asyncio/tasks.py 附近this is duplicated in _asynciomodule.c的说明仓库还保留了对照用例test_eager_start_true显式传loop任务立即完成与test_eager_start_false不急切启动任务先not done、协程体尚未执行见 Lib/test/test_asyncio/test_tasks.py。四、根因分析loop 的解析时机4.1Future.__init__负责解析 loopTask继承自futures._PyFuture其构造器Lib/asyncio/futures.py负责把loopNone解析为真实循环def __init__(self, *, loopNone): if self._loop is not None: raise RuntimeError(f{self.__class__.__name__} object is already initialized) if loop is None: self._loop events.get_event_loop() else: self._loop loop self._callbacks [] ...也就是说Task.__init__第一行的super().__init__(looploop)执行完毕后self._loop已经是解析后的真实事件循环对象当存在运行中的循环时get_event_loop()返回的就是它。4.2 缺陷本质使用了未解析的原始参数问题在于eager_start分支中直接引用了未经解析的原始loop参数其值可能是None而非基类解析后的self._loop。当调用方以Task(coro, eager_startTrue)方式构造时loop实参为None对该None值调用事件循环相关方法便抛出AttributeError。修复后的实现Lib/asyncio/tasks.py统一改用self._loop完成is_running()判断与call_soon()调度与Future基类的解析结果保持一致。从源码结构可以推断该缺陷只影响直接实例化任务且不传 loop这一条路径凡是经过asyncio.create_task、loop.create_task或任务工厂创建的任务loop均已由上层显式注入不受影响。这也解释了为什么该问题在常规使用中不易暴露却在自定义Task子类、动态构造任务等场景下会成为隐患。五、修复内容小结本次修复可以归纳为一条核心变更位置变更要点Lib/asyncio/tasks.pyeager_start分支改用基类解析后的self._loop替代原始loop参数保证is_running()与call_soon()作用于真实循环对象Lib/test/test_asyncio/test_tasks.py新增test_eager_start_true_no_loop回归测试锁定无显式 loop eager_startTrue场景Misc/NEWS.d/next/Core_and_Builtins/2026-07-25-17-35-14.gh-issue-154695.aQWLLv.rst记录面向用户可见的变更说明归属Core_and_Builtins新闻分类六、验证方法6.1 运行仓库自带的回归测试在 CPython 源码根目录构建出解释器后可运行相关测试# 运行整个 asyncio 任务测试模块 ./python -m test test_asyncio -m test_tasks # 或只运行本次修复对应的三个用例 ./python -m test test_asyncio -m test_tasks.test_eager_start_true \ test_tasks.test_eager_start_true_no_loop \ test_tasks.test_eager_start_false测试文件位于 Lib/test/test_asyncio/test_tasks.py其中test_eager_start_true_no_loop会同时覆盖 Python 与 C 两个 Task 实现。6.2 手动验证在已修复的版本上运行第三节的复现代码应正常输出结果 42 且不抛异常若希望观察急切启动行为可以在协程中加入打印对比eager_startTrue/False时协程体是立即执行还是下一轮循环才执行。七、对开发者的实践启示直接实例化Task时务必显式传loop即使本次缺陷已修复显式传入运行中的循环仍是更清晰、可预期的写法尤其是自定义Task子类应保持与Task.__init__兼容的签名含eager_start关键字参数。批量开启急切启动应使用任务工厂优先loop.set_task_factory(asyncio.eager_task_factory)而不是逐处手动传eager_startTrue工厂会自动注入loopLib/asyncio/tasks.py。理解loop的两级解析模型Future/Task构造时loopNone最终会落到events.get_event_loop()Lib/asyncio/futures.py因此任何在super().__init__之后使用循环的代码都应读取self._loop而不是回退到原始参数——这正是本次 bug 的教训所在。警惕工厂路径安全、直接构造路径裸奔的差异从 Lib/asyncio/base_events.py 可以看出loop.create_task对工厂与默认路径都会显式传loop排查类似问题时优先核对代码走的是哪条创建路径。结语gh-issue-154695 是一个小而典型的 CPython 缺陷案例eager_start机制本身设计合理但loop参数的解析时机差之毫厘便在直接构造任务的路径上留下了AttributeError隐患。修复以使用解析后的self._loop为原则并配套回归测试锁定行为。对于使用 asyncio 的开发者而言理解这条 NEWS 背后的调用链不仅能规避同类陷阱也能更透彻地掌握任务创建、急切启动与事件循环解析的完整图景。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考