You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

关于yield from/await语句未按预期挂起的技术问询

参考asyncio代码
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
asyncio.sleep核心实现
@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 05:27:23