You may be wondering how asyncio chooses which task to run and how it switches between tasks.
This is an important question and highlights how asyncio tasks are different from typical Python functions and thread-based concurrency.
In this tutorial, you will discover how asyncio switches between tasks and coroutines and how this is different from the way we switch between threads.
Let’s get started.
How Does Asyncio Switch Tasks?
A common question about asyncio that I’m asked is:
- How does Python switch between tasks in asyncio?
Given that many developers have some knowledge of threading, they might ask:
- Do asyncio tasks switch like Python threads.
This is a great question as it highlights the central difference between coroutines and threads.
To dig into this question we need to first review top-down forced context switching of threads in preemptive multitasking, and then contrast this to bottom-up volunteering in cooperating multitasking.
Run loops using all CPUs, download your FREE book to learn how.
Python Threads Context Switch
Python supports native threads that are managed by the underlying operating system.
The underlying operating system (e.g. Linux, MacOS, or Windows) manages which threads run at what time on the underlying CPU core hardware.
Typically, an operating system will be running many processes, each with one or more threads. There may be hundreds or thousands of threads running at a time as part of normal operation.
Not all threads are able to run at the same time. Instead, the operating system simulates multitasking by allowing each thread to run for a short amount of time before pausing the execution of the thread, storing its state, and switching to another thread.
The process of suspending one thread and reanimating a suspended thread is called a context switch.
In computing, a context switch is the process of storing the state of a process or thread, so that it can be restored and resume execution at a later point, and then restoring a different, previously saved, state.
— Context switch, Wikipedia.
Python threads will be context-switched by the operating system at any time, although typically when blocked, such as reading or writing from a file or socket.
The Python interpreter also offers a context switch interval configuration which is a fixed number of Python bytecode instructions (e.g. 100) or time interval (e,g. 100 milliseconds) that may be executed before the interpreter will explicitly offer a thread up for a context switch.
This is called preemptive multitasking and is the way that the operating system is able to manage and execute a large number of threads concurrently.
Preemptive multitasking involves the use of an interrupt mechanism which suspends the currently executing process and invokes a scheduler to determine which process should execute next. Therefore, all processes will get some amount of CPU time at any given time.
— Preemption (computing), Wikipedia.
You can learn more about context-switching threads and the context-switch interval in Python in the tutorial:
Asyncio Does Not Use Context Switching
Tasks and coroutines in asyncio are not context-switched by the operating system.
The asyncio event loop executes one task at a time.
A task will run for as long as it wants and then must explicitly yield control.
This will suspend the current task and allow the event loop to resume the execution of the next scheduled task.
This is called cooperative multitasking.
You can learn more about the explicit differences between asyncio and threading in the tutorial:
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.
What is Cooperative Multitasking
Cooperative multitasking is a concurrency model where multiple tasks or processes work together by voluntarily yielding control of the CPU to allow other tasks to execute.
Unlike preemptive multitasking, where an external scheduler forcibly switches between tasks based on a fixed time slice, cooperative multitasking relies on the cooperation of tasks to relinquish control.
Cooperative multitasking, also known as non-preemptive multitasking, is a style of computer multitasking in which the operating system never initiates a context switch from a running process to another process. Instead, in order to run multiple applications concurrently, processes voluntarily yield control periodically or when idle or logically blocked.
— Cooperative multitasking, Wikipedia.
In cooperative multitasking, each task must explicitly yield control to the scheduler or other tasks when it reaches a certain point or completes its current work. This requires tasks to be well-behaved and cooperative, as a task that does not yield control can monopolize the CPU and hinder the execution of other tasks.
One of the key advantages of cooperative multitasking is its simplicity and low overhead.
Context switching between tasks is more lightweight compared to preemptive multitasking, as it doesn’t involve saving and restoring the complete CPU state. However, the main drawback is that a misbehaving or poorly designed task can potentially disrupt the entire system by not yielding control, leading to unresponsiveness.
Cooperative multitasking is commonly used with non-blocking socket I/O, where it’s used to manage asynchronous operations without the need for explicit multi-threading.
Developers need to ensure that their tasks cooperate and yield control appropriately to maintain smooth execution in cooperative multitasking systems.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
How Can An Asyncio Task Yield Control
An asyncio task will yield control, or suspend when it awaits something.
Anytime a coroutine uses the “await” expression, such as on a task, it is signaling to the event loop that it is yielding control.
This might be for many reasons, such as:
- Waiting for a result from a task or coroutine.
- Waiting for a read or write operation on a socket or subprocess.
- Waiting for an asynchronous generator.
- Entering or exiting an asynchronous context manager.
A good place to start might be:
And:
In fact, looking closely at any asyncio program, you will see a given coroutine is littered with explicit and implicit await expressions, points where the current task may suspend and the coroutine will resume the next scheduled task.
An asyncio task may also explicitly yield control in the middle of a task that does not have any reasonable await points.
This can be achieved by awaiting a call to asyncio.sleep() for zero seconds.
This is a no-operation (nop) that signals the event loop that the current task is happy to yield control if any other scheduled tasks need an opportunity to run.
You can learn more about asyncio.sleep(0) in the tutorial:
What If Asyncio Tasks Don’t Yield Control
What if asyncio coroutines are selfish?
What if asyncio tasks never await anything?
This is a common concern by developers looking at asyncio through the lens of classical imperative or object-oriented programming. Asyncio is different, it uses an asynchronous programming model.
It is this difference in the framing of asyncio that causes the biggest misunderstandings of the module and in turn frustration when using it.
For example, see the tutorial:
Asyncio can fail to give up control. Coroutines can be selfish and uncooperative.
This can happen and it signals that an asyncio program has been written in a non-async manner. It does not use the tools of cooperating multitasking and may not be developed using the asynchronous programming model.
Asynchronous, in computer programming, refers to the occurrence of events independent of the main program flow and ways to deal with such events. These may be “outside” events such as the arrival of signals, or actions instigated by a program that take place concurrently with program execution, without the program blocking to wait for results.
— Asynchrony (computer programming), Wikipedia.
In this case, the program may need to be updated to be more cooperative.
You can learn more about Python asynchronous programming in the tutorial:
Perhaps the addition of asyncio.sleep(0) calls can be added.
Perhaps synchronous blocks of code can be executed in a thread that is external to the event loop and awaited on like an async task, using mechanisms like asyncio.to_thread().
You can learn more about executing blocking tasks in external threads in the tutorial:
And:
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 how asyncio switches between tasks and coroutines.
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 Eurico Craveiro on Unsplash
Do you have any questions?