关于yield from/await语句未按预期挂起的技术问询
import asyncio class A: async def noop(self): pass async def f(self, name): print(f"{name} -> await") await self.noop() print(f"{name} -> return") async def f1(self): await self.f("f1") async def f2(self): await self.f("f2") async def f3(self): await self.f("f3") async def __call__(self): await asyncio.gather(self.f1(), self.f2(), self.f3()) class B(A): async def noop(self): await asyncio.sleep(0) async def main(): a = A() print(a.__class__.__name__) await a() print() b = B() print(b.__class__.__name__) await b() asyncio.run(main())
A f1 -> await f1 -> return f2 -> await f2 -> return f3 -> await f3 -> return B f1 -> await f2 -> await f3 -> await f1 -> return f2 -> return f3 -> return
@types.coroutine def __sleep0(): """跳过一次事件循环运行周期。 这是`asyncio.sleep()`的私有辅助函数, 当`delay`设为0时使用。它使用一个裸`yield`表达式 (Task.__step知道如何处理),而非创建Future对象。 """ yield async def sleep(delay, result=None): ... if delay <= 0: await __sleep0() return result ...
asyncio的Task.__step逻辑显示:只有当生成器产出的对象为None或实现了Future类接口的类Future对象时,才会将控制权交还给事件循环。
asyncio的文档和示例给人的印象是,执行await表达式时协程总会被挂起。由于await obj等价于yield from obj.__await__(),而yield通常意味着“交出控制权/挂起”,那么yield from在生成器未产生任何值时不会挂起,这是否属于设计疏漏?
示例代码:
def outer(): def inner(v): if v: yield yield from inner(False) # 未挂起 yield from inner(True) # 已挂起
我理解用yield from挂起无产出的生成器并不现实,但“yield”的命名加上无异常静默执行的特性,导致我在代码中遇到任务未中途挂起的情况时产生了极大困惑。
另外,asyncio.sleep的文档提到了delay设为0的优化路径,但未详述其中的实现技巧:
将delay设置为0可提供优化路径,允许其他任务运行。这可用于长时间运行的函数,避免长时间阻塞事件循环。
这不属于设计疏漏,而是生成器与协程机制的固有行为,同时也是asyncio为性能做出的权衡。
1. yield from与无产出生成器的行为
yield from的本质是代理生成器的产出。如果被代理的生成器没有执行任何yield语句(或条件分支导致yield未触发),yield from会直接耗尽该生成器,不会触发挂起——因为没有任何“信号值”产出到事件循环,自然不会触发控制权移交。
对协程而言,await依赖__await__()返回的生成器:如果这个生成器没有产出任何内容,await会直接执行完毕,不会挂起协程。这完全符合生成器的语义,只是和“await总会挂起”的直觉认知冲突。
2. asyncio.sleep(0)触发挂起的核心逻辑
asyncio.sleep(0)能让协程交出控制权,关键在于它调用了__sleep0()——这个被@types.coroutine标记的生成器执行了裸yield,会产出一个None值。根据Task.__step的逻辑,产出None会被事件循环识别为“暂停当前任务,调度其他任务”的信号,从而实现让其他任务运行的效果。
3. 文档表述的局限性
文档中“await总会挂起”的表述是简化后的入门说明,实际仅适用于**会产生可等待对象(Future/Task/有产出的生成器)**的场景。await空协程(无任何await或yield的协程)时,协程会直接执行完毕,不会挂起。这种简化表述虽易造成误解,但并非设计疏漏——是为降低入门门槛牺牲了部分精确性。
总结
这类困惑的核心是混淆了“await”的直觉含义(挂起)与实际语义(等待可等待对象完成)。如果需要确保协程交出控制权,不要依赖空协程,应使用asyncio.sleep(0)。
内容的提问来源于stack exchange,提问作者random-user-3985747

