You can force the current asyncio task to suspend using asyncio.sleep(0).
This gives an opportunity for all other scheduled tasks in the event loop to run until their next point of suspension. This allows the event loop to progress one cycle through all tasks before resuming the current task.
In this tutorial, you will discover asyncio.sleep(0), what it is, and why it is important.
Let’s get started.
What is asyncio.sleep()?
The asyncio.sleep() call is a coroutine that suspends the current coroutine or task for a given number of seconds.
Block for delay seconds.
— Coroutines and Tasks
For example:
1 2 3 |
... # suspend for 2 seconds await asyncio.sleep(2) |
Or, less elegantly:
1 2 3 4 5 |
... # create coroutine coro = asyncio.sleep(2) # suspend for 2 seconds await coro |
We call asyncio.sleep() when we want to block or suspend the current coroutine for some time.
This might be to wait for a future time when some event has occurred.
It may also be used to simulate a blocking call such as in a prototype or unit test.
You can learn more about asyncio.sleep() in the tutorial:
Now that we know what asyncio.sleep() is, let’s consider the special case of asyncio.sleep(0).
Run loops using all CPUs, download your FREE book to learn how.
What is asyncio.sleep(0)?
The asyncio.sleep() coroutine provides a guarantee to always suspend the current coroutine or task.
sleep() always suspends the current task, allowing other tasks to run.
— Coroutines and Tasks
This is true, even if we provide the value of zero seconds, e.g. asyncio.sleep(0).
For example:
1 2 3 |
... # suspend the current task await asyncio.sleep(0) |
We must provide a value, e.g. 0, or perhaps a negative value. We cannot leave the call empty or provide a None value.
The asyncio.sleep(0) coroutine is a helpful as it allows:
- Other tasks have an opportunity to run.
- The current coroutine to resume as soon as other tasks are finished.
It provides a mechanism to allow one cycle of the asyncio event loop, allowing each scheduled task to progress until its next point of suspension (e.g. next blocking call).
Setting the delay to 0 provides an optimized path to allow other tasks to run. This can be used by long-running functions to avoid blocking the event loop for the full duration of the function call.
— Coroutines and Tasks
Another way to think about asyncio.sleep(0) is that it is a nop (no-op or no-operation).
It tells the event loop that the current task is happy to do nothing or suspend right now and grants every other scheduled task one opportunity to run before the current task resumes with regular activity.
Next, let’s consider when to use asyncio.sleep(0).
When to Use asyncio.sleep(0)?
We need to use asyncio.sleep(0) in our programs occasionally.
The most common use case is when:
- A new task is created and scheduled.
- We need to give the new task the opportunity to start running.
- The new task should start running before the current task resumes.
- We don’t want to wait for the new task to complete.
This may be the case if a new task is a helper task or a background task.
For example:
1 2 3 4 5 6 7 |
... # create and schedule task task = asyncio.create_task(other_work()) # suspend current task and allow new task to get started await asyncio.sleep(0) # carry on with things ... |
Without the asyncio.sleep(0) in the above snippet, the current task will create and schedule the new task, but the new task will not get an opportunity to start until the current task suspends or is done.
This could be a long time if the current task has many subtasks to complete. In this case, it can result in unexpected behavior if the task assumes that the newly created and scheduled task has started and is running in the background.
Some other use cases include:
- To explicitly give up control after performing a blocking task, such as file I/O or calling a CPU-intensive function.
- To explicitly give up control in a long task, that has many lines of code.
- To explicitly give up control in a loop repeated many times, e.g. a monitoring task.
Next, let’s see some other cases where asyncio.sleep(0) is used.
Where Else is asyncio.sleep(0) Used?
There are calls to asyncio.sleep(0) in the Python standard library and in third-party libraries.
For example, it is used when draining the StreamWriter class.
1 2 3 4 5 6 7 8 9 10 11 12 |
... if self._transport.is_closing(): # Wait for protocol.connection_lost() call # Raise connection closing error if any, # ConnectionResetError otherwise # Yield to the event loop so connection_lost() may be # called. Without this, _drain_helper() would return # immediately, and code that calls # write(...); await drain() # in a loop would never call connection_lost(), so it # would not see an error when the socket is closed. await sleep(0) |
It is also used in the asyncio.Server class.
1 2 3 4 5 |
async def start_serving(self): self._start_serving() # Skip one loop iteration so that all 'loop.add_reader' # go through. await tasks.sleep(0) |
Is asyncio.sleep(0) a Hack?
No.
The call to await asyncio.sleep(0) looks like a hack.
It’s not a hack, it is just the way the Python standard library chose to offer this required functionality.
- It is a documented pattern to allow the current task to give up control and suspend.
- It signals the event loop to give an opportunity to all other scheduled tasks.
- It ensures that the current task will execute as soon as the event loop is able to return control
There was some debate first about how to (re-)introduce this ability into asyncio: “Question: How to relinquishing control to the event loop in Python 3.5“.
For example:
… In Python < 3.5, you could do a yield or yield None in a coroutine to give control to the event loop. In Python 3.5, it is invalid to yield in an async def coroutine. So, what’s the proper way of relinquishing control to the event loop?
— Question: How to relinquishing control to the event loop in Python 3.5
Then a discussion on how to best describe this feature in the API documentation: “asyncio.sleep(0) not documented“
… Looking at the implementation and at the old issue at python/asyncio#284 shows that asyncio.sleep special-cases asyncio.sleep(0) to mean “yield control to the event loop” without incurring additional overhead of sleeping. However, this is not documented …
— asyncio.sleep(0) not documented
Now that we know what asyncio.sleep(0) is and why it is important, let’s look at some worked examples.
Example Without asyncio.sleep(0)
Before we look at an example of asyncio.sleep(0), let’s develop an example without it.
We can contrive a situation that breaks when we don’t have asyncio.sleep(0) and works when we add it in.
In this case, we have a background task that prepares some data. The main task needs this data for some reason but does not want to explicitly wait for the task to complete. We want the other task to run in the background.
1 2 3 4 5 6 |
# a background task async def other_task(): # report a message print('Other task is running') # prepare some data and return it return 99 |
If we assume that the background task will just start running and will be ready when we need it, we will get an error.
1 2 3 4 5 6 7 8 9 10 |
# entry point coroutine async def main(): # create and schedule the new task task = asyncio.create_task(other_task()) # do some other things # ... # get the result from by other task result = task.result() # report the result print(result) |
This is because the new task was created and scheduled, but is not given an opportunity to run.
When we try to retrieve the result from this task, we expect an exception, specifically as InvalidStateError because the task is not done, it is not even started. It is pending.
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# SuperFastPython.com # example of starting a new task without asyncio.sleep(0) import asyncio # a background task async def other_task(): # report a message print('Other task is running') # prepare some data and return it return 99 # entry point coroutine async def main(): # create and schedule the new task task = asyncio.create_task(other_task()) # do some other blocking things data = [i*i for i in range(10000000)] # get the result from by other task result = task.result() # report the result print(result) # start the asyncio program asyncio.run(main()) |
Running the example first starts the event loop and runs the main() coroutine.
The main() coroutine creates the other_task() coroutine, then creates a task and schedules it for this coroutine.
This task does not yet run.
The main() coroutine then does some CPU-bound task that blocks the event loop.
It then checks on the result of the background task.
This fails with an exception because the background task has not had the opportunity to execute. It is scheduled and is pending.
This highlights a common problem where new tasks are created and scheduled but not given an opportunity to execute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Other task is running Traceback (most recent call last): File "...", line 24, in <module> asyncio.run(main()) File ".../asyncio/runners.py", line 190, in run return runner.run(main) ^^^^^^^^^^^^^^^^ File ".../asyncio/runners.py", line 118, in run return self._loop.run_until_complete(task) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../asyncio/base_events.py", line 653, in run_until_complete return future.result() ^^^^^^^^^^^^^^^ File "...", line 19, in main result = task.result() ^^^^^^^^^^^^^ asyncio.exceptions.InvalidStateError: Result is not set. |
Next, let’s look at an example that fixes this situation by adding a asyncio.sleep(0).
Free Python Asyncio Course
Download your FREE Asyncio PDF cheat sheet and get BONUS access to my free 7-day crash course on the Asyncio API.
Discover how to use the Python asyncio module including how to define, create, and run new coroutines and how to use non-blocking I/O.
Example With asyncio.sleep(0)
We can explore how to update the above example to work as expected by adding an asyncio.sleep(0).
Firstly, we note that the main() coroutine could just await the new task.
Perhaps after it is created, or perhaps right before the result is needed.
For example:
1 2 3 |
... # get the result from by other task result = await task |
We won’t do this.
In this contrived situation, we require the caller to not block on the new task for some reason. It doesn’t want to wait on the new task.
Instead, the solution is for the caller to give up control for one cycle of the event loop.
This allows all other scheduled tasks, like our background task, an opportunity to progress until their next point of suspension. In the case of the background task, it will complete.
This can be achieved by adding in a call to asyncio.sleep(0) somewhere, such as right after the new task is created and scheduled.
1 2 3 |
... # suspend for a moment and allow all scheduled tasks to run await asyncio.sleep(0) |
The caller is not blocking on the background task directly. It is allowing all other scheduled tasks an opportunity to progress, which includes our background task.
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# SuperFastPython.com # example of starting a new task without asyncio.sleep(0) import asyncio # a background task async def other_task(): # report a message print('Other task is running') # prepare some data and return it return 99 # entry point coroutine async def main(): # create and schedule the new task task = asyncio.create_task(other_task()) # suspend for a moment and allow all scheduled tasks to run await asyncio.sleep(0) # do some other blocking things data = [i*i for i in range(10000000)] # get the result from by other task result = task.result() # report the result print(result) # start the asyncio program asyncio.run(main()) |
Running the example first starts the event loop and runs the main() coroutine.
The main() coroutine creates the other_task() coroutine, then creates a task and schedules it for this coroutine.
It then suspends with asyncio.sleep(0) and the event loop to progress one cycle.
The background task is given a chance to run and is completed.
The main() coroutine resumes and then does some CPU-bound task that blocks the event loop.
It then checks on the result of the background task.
This succeeds as the result is now available.
This highlights how we can fix a common problem in asyncio that allows newly created and scheduled tasks an opportunity to get started from the creator of those new tasks.
1 2 |
Other task is running 99 |
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Further Reading
This section provides additional resources that you may find helpful.
Python Asyncio Books
- Python Asyncio Mastery, Jason Brownlee (my book!)
- Python Asyncio Jump-Start, Jason Brownlee.
- Python Asyncio Interview Questions, Jason Brownlee.
- Asyncio Module API Cheat Sheet
I also recommend the following books:
- Python Concurrency with asyncio, Matthew Fowler, 2022.
- Using Asyncio in Python, Caleb Hattingh, 2020.
- asyncio Recipes, Mohamed Mustapha Tahrioui, 2019.
Guides
APIs
- asyncio — Asynchronous I/O
- Asyncio Coroutines and Tasks
- Asyncio Streams
- Asyncio Subprocesses
- Asyncio Queues
- Asyncio Synchronization Primitives
References
Takeaways
You now know about asyncio.sleep(0), what it is, and why it is important.
Did I make a mistake? See a typo?
I’m a simple humble human. Correct me, please!
Do you have any additional tips?
I’d love to hear about them!
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Eugene Zhyvchik on Unsplash
Do you have any questions?