Search code examples
pythonasync-awaitcoroutinepython-asyncio

Python coroutines don't run concurrently with time.sleep()?


I'm trying to follow the examples at https://docs.python.org/3/library/asyncio-task.html#coroutines; here is the code snippet which runs two say_after coroutines concurrently:

import asyncio
import time


async def say_after(delay, what):
    await asyncio.sleep(delay)
    # time.sleep(delay)
    print(what)


async def main():
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))


    print(f"Started at {time.strftime('%X')}")

    await task1
    await task2

    print(f"Finished at {time.strftime('%X')}")


if __name__ == "__main__":
    asyncio.run(main())

If I run this, I get that the start and end are two seconds apart:

Started at 12:59:35
hello
world
Finished at 12:59:37

However, if I replace await asyncio.sleep(delay) with time.sleep(delay) (the commented-out line in the snippet above), I get that they are three seconds apart, and hence essentially running synchronously:

Started at 13:00:53
hello
world
Finished at 13:00:56

I don't quite understand this; isn't the point of having concurrent tasks that they run in parallel, even if the tasks themselves contain synchronous code? Why does this example no longer work with time.sleep() instead of asyncio.sleep()?


Solution

  • I don't quite understand this; isn't the point of having concurrent tasks that they run in parallel, even if the tasks themselves contain synchronous code?

    Concurrency != Parallelism. When writing asyncio code, the underlying routines still have to yield flow back to the eventloop in order to allow concurrency. And the GIL is still there, regardless.

    Why does this example no longer work with time.sleep() instead of asyncio.sleep()?

    The difference here is that asyncio.sleep suspends the current task, allowing other tasks to run. time.sleep does not, it's a "blocking" call. Programs using asyncio are still single-threaded (unless otherwise using threading), meaning time.sleep blocks execution in the main thread and blocks the entire program until the sleep duration has elapsed.

    Coroutines afford cooperative concurrency, not parallelism.

    To achieve good concurrency through coroutines, any code called within asyncio.run must be written in a non-blocking way. In practice, it means that any code run within a task has the responsibility to yield when it is a good time to pause execution, e.g. "I'm not doing anything useful because I'm just waiting on I/O" allowing another task to use the event loop.