5

When I run it on cpython 3.6, the following program prints hello world a single time and then spins forever.

As a side note, uncommenting the await asyncio.sleep(0) line causes it to print hello world every second, which is understandable.

import asyncio

async def do_nothing():
    # await asyncio.sleep(0)
    pass

async def hog_the_event_loop():
    while True:
        await do_nothing()

async def timer_print():
    while True:
        print("hello world")
        await asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.create_task(timer_print())
loop.create_task(hog_the_event_loop())
loop.run_forever()

This behavior (printing hello world a single time) makes sense to me, because hog_the_event_loop never blocks and therefore has no need to suspend execution. Can I rely on this behavior? When the line await do_nothing() runs, is it possible that rather than entering the do_nothing() coroutine, execution will actually suspend and resume timer_print(), causing the program to print hello world a second time?

Put more generally: When will python suspend execution of a coroutine and switch to another one? Is it potentially on any use of the await keyword? or is it only in cases where this results in an underlying select call (such as I/O, sleep timers, etc)?

Additional Clarification

I understand that if hog_the_event_loop looked like this, it would certainly never yield execution to another coroutine:

async def hog_the_event_loop():
    while True:
        pass

I'm trying to specifically get at the question of whether await do_nothing() is any different than the above.

E_G
  • 327
  • 3
  • 9
  • 2
    Without the `await` line your `hog_the_event_loop` has only synchronous code in an endless loop. That will block the event loop. With the line on every iteration an `await` will be reached and the event loop can leave the corouting and execute an other waiting task. – Klaus D. May 19 '19 at 01:34
  • 1
    Thank you. Just to make sure I understand: the mere use of `await` in `await do_nothing()` doesn't qualify as asynchronous code, and it won't be sufficient to cause another task to execute, as `await asyncio.sleep(0)` would? – E_G May 19 '19 at 01:37
  • 2
    There has to be something awaitable involved. – Klaus D. May 19 '19 at 01:42
  • I see. But getting at this edge case, does `await do_nothing()` count as being cooperative? since it is indeed using the `await` keyword? – E_G May 19 '19 at 01:50
  • 2
    Some context that I sort of remember from David Beazley (but details are fuzzy, so I'll leave a comment rather than answer): the `async/await` model is an example of cooperative multitasking: a function is implemented in a manner to signal the points in the function's execution when it is appropriate or useful to yield control back to the event loop; and the function uses `await` to send that signal. A function without `await` is not being "cooperative", so to speak. – FMc May 19 '19 at 01:52
  • 2
    Regarding your follow-up, `await do_nothing()` establishes the expectation that `do_nothing()` will also participate in the cooperative regime. Since it doesn't, the endless loop in `hog_the_event_loop()` never gives up control. That at least is my intuitive understanding of this; it's been a while since I spent a lot of time with it. – FMc May 19 '19 at 01:54

2 Answers2

5

This behavior (printing hello world a single time) makes sense to me, because hog_the_event_loop never blocks and therefore has no need to suspend execution. Can I rely on this behavior?

Yes. This behavior directly follows from how await is both specified and implemented by the language. Changing it to suspend without the awaitable object having itself suspended would certainly be a breaking change, both for asyncio and other Python libraries based in async/await.

Put more generally: When will python suspend execution of a coroutine and switch to another one? Is it potentially on any use of the await keyword?

From the caller's perspective, any await can potentially suspend, at the discretion of the awaited object, also known as an awaitable. So the final decision of whether a particular await will suspend is on the awaitable (or on awaitables it awaits itself if it's a coroutine, and so on). Awaiting an awaitable that doesn't choose to suspend is the same as running ordinary Python code - it won't pass control to the event loop.

The leaf awaitables that actually suspend are typically related to IO readiness or timeout events, but not always. For example, awaiting a queue item will suspend if the queue is empty, and awaiting run_in_executor will suspend until the function running in the other thread completes.

It is worth mentioning that asyncio.sleep is explicitly guaranteed to suspend execution and defer to the event loop, even when the specified delay is 0 (in which case it will immediately resume on the next event loop pass).

user4815162342
  • 141,790
  • 18
  • 296
  • 355
3

No, await do_nothing() will never suspend. await propagates suspension from an awaitable by suspending the surrounding coroutine in response. But when the awaitable is already ready, there’s nothing to wait for and execution continues from the await (with a return value, in general). A nother way of thinking about “nothing to wait for” is that the event loop literally has no object on which to base the timing of resuming from a notional suspension; even “resume as soon as other pending tasks suspend” is a schedule that would have to be expressed as some object (e.g., sleep(0)).

A coroutine that does nothing is always ready, just like one that sleeps is ready after the time elapses. Put differently, a coroutine that just sleeps N times suspends N times—even if N is 0.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76