4

Could somebody please explain why there is a 5 second delay between coro2 finishing and coro1 finishing?

Also, why is there no such delay if I replace asyncio.wait([asyncio.sleep(5)]) with asyncio.sleep(5)?

async def coro1():
    logger.info("coro1 start")
    await asyncio.wait([asyncio.sleep(5)])
    logger.info("coro1 finish")

async def coro2():
    logger.info("coro2 start")
    time.sleep(10)
    logger.info("coro2 finish")

async def main():
    await asyncio.gather(coro1(), coro2())

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
2020-05-25 12:44:56 coro1 start
2020-05-25 12:44:56 coro2 start
2020-05-25 12:45:06 coro2 finish
2020-05-25 12:45:11 coro1 finish
1
  • 1
    Don't use time.sleep(10) in a coroutine. It blocks the entire event loop and all other coroutines. Commented May 25, 2020 at 12:02

1 Answer 1

4

TLDR: Do not use blocking calls such as time.sleep in a coroutine. Use asyncio.sleep to asynchronously pause, or use an event loop executor if blocking code must be run.


Using asyncio.wait([thing]) adds a level of indirection, executing thing in a new Future/Task. While a bare await asyncio.sleep(5) executes the sleep during coro1, the wrapped await asyncio.wait([asyncio.sleep(5)]) executes the sleep after all other currently scheduled coroutines.

async def coro1():
    logger.info("coro1 start")
    await asyncio.sleep(5)   # started immediately
    logger.info("coro1 finish")

async def coro1():
    logger.info("coro1 start")
    await asyncio.wait([     # started immediately
        asyncio.sleep(5)     # started in new task
    ])
    logger.info("coro1 finish")

Since coro2 uses the blocking time.sleep(10), it disables the event loop and all other coroutines.

async def coro2():
    logger.info("coro2 start")
    time.sleep(10)           # nothing happens for 10 seconds
    logger.info("coro2 finish")

This prevents further Futures from being started - including new future from asyncio.wait - and from being resumed - including the bare asyncio.sleep(5). In the former case, that means the asyncio.sleep starts after the time.sleep is done - therefore taking 10 + 5 seconds to complete. In the latter case, that means the asyncio.sleep has already started, it just cannot complete before the 10 seconds are up - therefore taking max(10, 5) seconds to complete.


Consistently use asyncio.sleep to get the desired durations. If blocking code must be executed, have it run via an executor.

async def coro1w():
    print("coro1w start", time.asctime())
    await asyncio.wait([asyncio.sleep(5)])
    print("coro1w finish", time.asctime())

async def coro1b():
    print("coro1b start", time.asctime())
    await asyncio.sleep(5)
    print("coro1b finish", time.asctime())

async def coro2a():
    print("coro2a start", time.asctime())
    await asyncio.sleep(10)            # asynchronous sleep
    print("coro2a finish", time.asctime())

async def coro2t():
    print("coro2t start", time.asctime())
    loop = asyncio.get_running_loop()  # threaded sleep
    await loop.run_in_executor(None, lambda: time.sleep(10))
    print("coro2t finish", time.asctime())

async def main():
    await asyncio.gather(coro1w(), coro1b(), coro2a(), coro2t())

asyncio.run(main())
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks so much for your answer. I know you shouldn't use time.sleep when you could instead use asyncio.sleep. I was using it to simulate unavoidable blocking code. Is there any way to avoid the delay whilst still using time.sleep?
All Python async frameworks allow to run blocking code in threads. For asycio, use asyncio.loop.run_in_executor.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.