7

I'm reading 'Fluent Python' by 'Luciano Ramalho' over and over, but I couldn't understand asyncio.sleep's behavior inside asyncio.

Book says at one part:

Never use time.sleep in asyncio coroutines unless you want to block the main thread, therefore freezing the event loop and probably the whole application as well. (...) it should yield from asyncio.sleep(DELAY).

On the other part:

Every Blocking I/O function in the Python standard library releases the GIL (...) The time.sleep() function also releases the GIL.

As time.sleep() releases GIL codes on other thread can run, but blocks current thread. Since asyncio is single-threaded, I understand that time.sleep blocks asyncio loop.

But, how asyncio.sleep() isn't blocking thread? Is it possible to not delay event loop and wait at the same time?

jupiterbjy
  • 2,882
  • 1
  • 10
  • 28
  • 3
    `asyncio.sleep()` doesn't actually sleep. It hands back control and schedules a "re-call" for continuation. It works more like a `yield`. – Klaus D. Jun 21 '20 at 02:20
  • Maybe [this](https://stackoverflow.com/a/52026721/1720199) can help. – cglacet Jun 21 '20 at 02:22
  • @KlausD. That one line made lots of sense, much appreciated. Would it be better to close this question now? – jupiterbjy Jun 21 '20 at 02:31
  • @cglacet I Was having hard time inside asyncio module already with pycharm. It was my brain that couldn't figured it out how it works as a whole. – jupiterbjy Jun 21 '20 at 02:33
  • 2
    The important thing in this code is [call_later](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.call_later). Instead of `time.sleep` which suspend the execution for `x` seconds, `asyncio.sleep` simply register a future to be called in `x` seconds (and also hands control back to the [event loop](https://docs.python.org/3/library/asyncio-eventloop.html#event-loop)). – cglacet Jun 21 '20 at 02:40

2 Answers2

7

Under the hood, asyncio has an "event loop": it's a function that loops over queue of tasks. When you add new task, it's added in the queue. When task yields, it gets suspended and event loop moves onto next task. Suspended tasks are ignored until they resume. When task finishes, it gets removed from the queue.

For example, when you call asyncio.run, it adds new task into queue and then enters event loop until there are no more tasks.

Few quotes from official documentation:

The event loop is the core of every asyncio application. Event loops run asynchronous tasks and callbacks, perform network IO operations, and run subprocesses.

Event loops use cooperative scheduling: an event loop runs one Task at a time. While a Task awaits for the completion of a Future, the event loop runs other Tasks, callbacks, or performs IO operations.

When you call asyncio.sleep, it suspends current task, thus allowing other tasks to run. Well, I am basically retelling the documentation:

sleep() always suspends the current task, allowing other tasks to run.

Shadows In Rain
  • 1,140
  • 12
  • 28
7

The function asyncio.sleep simply registers a future to be called in x seconds while time.sleep suspends the execution for x seconds.

You can test how both behave with this small example and see how asyncio.sleep(1) doesn't actually give you any clue on how long it will "sleep" because it's not what it really does:

import asyncio 
import time
from datetime import datetime


async def sleep_demo():
    print("async sleep start 1s: ", datetime.now().time())
    await asyncio.sleep(1)
    print("async sleep end: ", datetime.now().time())
    

async def I_block_everyone():
    print("regular sleep start 3s: ", datetime.now().time())
    time.sleep(3)
    print("regular sleep end: ", datetime.now().time())
    
    
asyncio.gather(*[sleep_demo(), I_block_everyone()])

This prints:

async sleep start 1s:        04:46:55
regular sleep start 3s:      04:46:55
regular sleep end:           04:46:58
async sleep end:             04:46:58 

The blocking call time.sleep prevent the event loop from scheduling the future that resumes sleep_demo. In the end, it gains control back only after approximately 3 seconds (even though we explicitly requested a 1 second async sleep).

Now concerning "The time.sleep() function also releases the GIL.", this is not a contradiction as it will only allow another thread to execute (but the current thread will remain pending for x seconds). Somewhat both look a bit similar, in one case the GIL is released to make room for another thread, in asyncio.sleep, the event loop gains control back to schedule another task.

cglacet
  • 8,873
  • 4
  • 45
  • 60