5

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()?

3
  • 5
    Why do you think asyncio.sleep() would even exist, if time.sleep() actually worked in an async context? time.sleep() blocks the whole thread. Commented Aug 19, 2019 at 20:07
  • Non-blocking calls are needed, Time.sleep blocks the thread. Commented Aug 19, 2019 at 20:10
  • time.sleep is blocking Commented Aug 19, 2019 at 21:01

1 Answer 1

5

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.

Sign up to request clarification or add additional context in comments.

3 Comments

An important point to make clear: asyncio is cooperative concurrency; if you don't do something to explicitly yield control back to the event loop (e.g. with await) then the event loop won't get a chance to run anything else until your code is done. If you choose to block on something that's not an async awaitable thing, the event loop has no way to know it could do something else, so it just keeps waiting for you to yield control back to it.
Compare and contrast with some multitasking implementations, where new tasks can interrupt ("context switch") running ones before they've finished, instead of waiting for them to end.
Incidentally, the difference between asyncio.sleep() and time.sleep() is also explained here: realpython.com/async-io-python.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.