【问题标题】:asyncio.sleep(0) does not yield control to the event loopasyncio.sleep(0) 不会将控制权交给事件循环
【发布时间】:2022-11-19 16:29:16
【问题描述】:

我有一个简单的异步设置,其中包括两个协程:light_job 和 heavy_job。 light_job 在中间停止,heavy_job 开始。我希望 heavy_job 在中间让出控制权并允许 light_job 完成,但 asyncio.sleep(0) 没有像我预期的那样工作。

这是设置:

import asyncio
import time

loop = asyncio.get_event_loop()


async def light_job():
    print("hello ")
    print(time.time())
    await asyncio.sleep(1)
    print(time.time())
    print("world!")


async def heavy_job():
    print("heavy start")
    time.sleep(3)
    print("heavy halt started")
    await asyncio.sleep(0)
    print("heavy halt ended")
    time.sleep(3)
    print("heavy done")

loop.run_until_complete(asyncio.gather(
    light_job(),
    heavy_job()
))

如果我运行这段代码,light_job 将不会继续,直到 heavy_job 完成。这是输出:

hello 
1668793123.159075
haevy start
heavy halt started
heavy halt ended
heavy done
1668793129.1706061
world!

但如果我将 asyncio.sleep(0) 更改为 asyncio.sleep(0.0001),代码将按预期工作:

hello 
1668793379.599066
heavy start
heavy halt started
1668793382.605899
world!
heavy halt ended
heavy done

根据文档和related threads,我希望 asyncio.sleep(0) 与 asyncio.sleep(0.0001) 完全一样。这里有什么?

【问题讨论】:

  • time.sleep(3) 将使整个程序进入休眠状态,因此不会执行任何其他操作。 asyncio 并不意味着执行 CPU 绑定任务 - 为此使用线程或 multiprocessing
  • 我有意使用 time.sleep(用于模拟对 cpu 要求很高的任务)。我的问题是为什么 asyncio.sleep(0) (在两次 time.sleeps 之间)不产生控制而 asyncio.sleep(0.0..01) 产生。
  • 因为当你运行time.sleep(3)时,整个脚本都会进入休眠状态,而await asyncio.sleep(0)只会在 3 秒后执行。
  • 正如我所说,我是故意的。我希望 heavy_job 保持控制 3 秒(time.sleep),而不是将其传递给 light_job(asyncio.sleep),然后再次保持 3 秒(time.sleep)。当我使用 asyncio.sleep(0.0001) 时它会这样做,但当我使用 asyncio.sleep(0) 时则不会。我的问题是关于这两者之间的区别。
  • 它们在 asyncio 内部代码中的处理方式有所不同,但每份文档、论坛线程、SO 问题等。我发现似乎都在告诉我它们最终应该采取相同的行动。我可以重现您看到的结果;不知道为什么。

标签: python python-asyncio


【解决方案1】:

拨打asyncio.sleep(0) 3次:

import asyncio
import time


async def light_job():
    print("hello ")
    print(time.time())
    await asyncio.sleep(1)
    print(time.time())
    print("world!")


async def heavy_job():
    print("heavy start")
    time.sleep(3)
    print("heavy halt started")
    for _ in range(3):
        await asyncio.sleep(0)
    print("heavy halt ended")
    time.sleep(3)
    print("heavy done")


async def test():
    await asyncio.gather(
        light_job(),
        heavy_job()
    )

asyncio.run(test())

这导致:

hello 
1668844526.157173
heavy start
heavy halt started
1668844529.1575627
world!
heavy halt ended
heavy done

查看“asyncio/base_events.py”,“_run_once”首先遍历挂起的计时器,然后在计算之后运行它看到的所有内容。 asyncio.sleep 只能跳过事件循环的一次迭代。需要多次睡眠,因为 asyncio.sleep(1) 安排了一个未来,在通过将 light_job 添加回队列将控制权交还给 light_job 之前,需要进行一次额外的迭代,而 asyncio 恰好最后运行新排队的作业。

为了更清晰的图片,可以添加更多的打印语句:

import asyncio
import time


async def light_job():
    print("hello ")
    print(time.time())
    await asyncio.sleep(1)
    print(time.time())
    print("world!")


async def heavy_job():
    print("heavy start")
    time.sleep(3)
    print("heavy halt started")
    # Sleep to yield to the event loop. light_job isn't detected as ready so this iteration of the loop will finish
    await asyncio.sleep(0)

    print("after 1 sleep")
    # We are still in front of the event loop. Yield so that the 1 second timer in light_job runs.
    # The timer will realize it itself has expired, then put light_job back onto the queue.
    await asyncio.sleep(0)

    # Again the current Python implementation puts us in front. Yield so that the light_job runs
    print("after 2 sleeps")
    await asyncio.sleep(0)

    print("heavy halt ended")
    time.sleep(3)
    print("heavy done")


async def test():
    await asyncio.gather(
        light_job(),
        heavy_job()
    )

asyncio.run(test())

然后在“asyncio/base_events.py”的“def _run_once(self):”中添加断点。在第 1842 行的开头添加断点打印“loop start”,也就是“sched_count =”。在末尾的第 1910 行添加另一个,也就是“handle = None”打印“loop end”。然后在第 1897 行运行每个任务之前添加一个,也就是“if self._debug:”评估和打印“_format_handle(handle)”。事件的顺序被揭示:

loop start
<Task pending name='Task-1' coro=<test() running at /home/home/PycharmProjects/sandbox/notsync.py:34> cb=[_run_until_complete_cb() at /usr/lib/python3.11/asyncio/base_events.py:180]>
loop end
loop start
<Task pending name='Task-2' coro=<light_job() running at /home/home/PycharmProjects/sandbox/notsync.py:5> cb=[gather.<locals>._done_callback() at /usr/lib/python3.11/asyncio/tasks.py:759]>
hello 
1668844827.5052986
<Task pending name='Task-3' coro=<heavy_job() running at /home/home/PycharmProjects/sandbox/notsync.py:13> cb=[gather.<locals>._done_callback() at /usr/lib/python3.11/asyncio/tasks.py:759]>
heavy start
heavy halt started
loop end
loop start
<Task pending name='Task-3' coro=<heavy_job() running at /home/home/PycharmProjects/sandbox/notsync.py:18> cb=[gather.<locals>._done_callback() at /usr/lib/python3.11/asyncio/tasks.py:759]>
after 1 sleep
<TimerHandle when=37442.097934711 _set_result_unless_cancelled(<Future pendi...ask_wakeup()]>, None) at /usr/lib/python3.11/asyncio/futures.py:317>
loop end
loop start
<Task pending name='Task-3' coro=<heavy_job() running at /home/home/PycharmProjects/sandbox/notsync.py:23> cb=[gather.<locals>._done_callback() at /usr/lib/python3.11/asyncio/tasks.py:759]>
after 2 sleeps
<Task pending name='Task-2' coro=<light_job() running at /home/home/PycharmProjects/sandbox/notsync.py:8> wait_for=<Future finished result=None> cb=[gather.<locals>._done_callback() at /usr/lib/python3.11/asyncio/tasks.py:759]>
1668844830.9250844
world!
loop end
loop start
<Task pending name='Task-3' coro=<heavy_job() running at /home/home/PycharmProjects/sandbox/notsync.py:27> cb=[gather.<locals>._done_callback() at /usr/lib/python3.11/asyncio/tasks.py:759]>
heavy halt ended
heavy done
<Handle gather.<locals>._done_callback(<Task finishe...> result=None>) at /usr/lib/python3.11/asyncio/tasks.py:759>
loop end
loop start
<Handle gather.<locals>._done_callback(<Task finishe...> result=None>) at /usr/lib/python3.11/asyncio/tasks.py:759>
loop end
loop start
<Task pending name='Task-1' coro=<test() running at /home/home/PycharmProjects/sandbox/notsync.py:35> wait_for=<_GatheringFuture finished result=[None, None]> cb=[_run_until_complete_cb() at /usr/lib/python3.11/asyncio/base_events.py:180]>
loop end
loop start
<Handle _run_until_complete_cb(<Task finishe...> result=None>) at /usr/lib/python3.11/asyncio/base_events.py:180>
loop end
loop start
<Task pending name='Task-4' coro=<BaseEventLoop.shutdown_asyncgens() running at /usr/lib/python3.11/asyncio/base_events.py:539> cb=[_run_until_complete_cb() at /usr/lib/python3.11/asyncio/base_events.py:180]>
loop end
loop start
<Handle _run_until_complete_cb(<Task finishe...> result=None>) at /usr/lib/python3.11/asyncio/base_events.py:180>
loop end
loop start
<Task pending name='Task-5' coro=<BaseEventLoop.shutdown_default_executor() running at /usr/lib/python3.11/asyncio/base_events.py:564> cb=[_run_until_complete_cb() at /usr/lib/python3.11/asyncio/base_events.py:180]>
loop end
loop start
<Handle _run_until_complete_cb(<Task finishe...> result=None>) at /usr/lib/python3.11/asyncio/base_events.py:180>
loop end

【讨论】:

  • 虽然这是一项了不起的侦探工作,但作为一种解决方案,它是巫毒教……不要用对asyncio.sleep(0)的循环调用来乱丢你的代码库
猜你喜欢
  • 1970-01-01
  • 2012-10-24
  • 2021-11-02
  • 1970-01-01
  • 2017-03-30
  • 1970-01-01
  • 2019-04-22
  • 1970-01-01
  • 2015-01-06
相关资源
最近更新 更多