Written by Ivan Knezović and Tonino Kaštelan
Why another async blog
Earlier this year we attended the PyCon Italia, where we had opportunities to witness some really interesting topics and talks. One that caught our eye was the talk called The Hitchhiker’s guide to Asyncio by Fabbiani (great fellow 😁). Due to the surprisingly large number of people who attended the talk, more than half of us had to sit on the floor. Obviously, there was a huge amount of people who either did not understand this topic or sought to understand it better. Through the talk, we were pleasantly surprised with the approach used. Instead of jumping into technical jargon and explaining what is happening in the background, the presenter used analogies to bring the topic closer to the audience. This inspired us to delve deeper into the topic, understand it better ourselves by explaining it to each other through analogies, and finally after completely simplifying it, bring it to you, to hopefully better your understanding of this topic.
What is asynchronous programming?
Asynchronous programming is a programming paradigm that enables you to write code without blocking the main thread of execution. It might sound complex, but it is pretty much straightforward. This means that your program can take a break or do something else while the asynchronous task is running. Let’s illustrate this concept with a simple example.
Meet Little Tommy Sync. Tommy had been tasked to wash the dishes before he could go outside and play. So, being a good boy, he picks up the dishes and starts washing them one by one. Once he finishes, he can finally go outside and play.
On the other hand, we have Async Bob. Bob realizes he has a dishwasher, so he gathers all the dirty dishes, places them in the dishwasher and turns it on. Now, with some free time on his hands, he relaxes and plays video games for a while. Later, he takes the dishes out of the dishwasher and heads outside to play with Tommy.
Notice how both Tommy and Bob completed their chores at approximately the same time. However, Bob had more time to relax and do other things while the dishwasher was doing the work.
That’s precisely the essence of asynchronous programming — utilizing available resources in the most efficient manner. In the world of programming, we can harness this power to enable other tasks to run and reach completion while we patiently wait for a background task to wrap up its execution.
So how could we translate this to Python?
As in Python ync
The Asyncio library in Python provides a powerful framework for asynchronous programming. It allows you to easily write concurrent code that can efficiently handle multiple tasks without blocking the execution flow.
The core concept behind Asyncio is the use of coroutines, which are functions that can be paused and resumed, allowing other tasks to be executed in the meantime. And all of those tasks are orchestrated by an event loop. Together they make the basic building blocks of the Asyncio library. Let’s again use an analogy to simplify this concept.
Imagine a fantastic orchestra where the event loop plays the part of the conductor, just like Tommy guiding the show. In this musical world, Bob and Ricardo, the coroutines, step into the spotlight as skilled musicians. They effortlessly perform their parts, pausing, switching and resuming with ease, adding a delightful rhythm to the performance. Tommy, with his expertise, directs the event loop, flawlessly coordinating and executing tasks, creating a mesmerizing and well-orchestrated spectacle that enchants the audience.
Jumping back to the real world now, let’s take a look at a diagram that demonstrates how this works in Asyncio.
It might look daunting at first, but we will simplify this a lot pretty quickly.
The event loop has a queue in which it accepts incoming tasks. The event loop pulls the tasks out of the queue and runs them.
Let’s take a look at the lifecycle of Task A:
Task A is taken out of the queue
The coroutine of task A has started and now controls the resources
The coroutine comes across an async operation (IE: reading a file) and it pauses it’s execution and gives control back to the event loop, which then registers a callback for the coroutine
Event loop now chooses task B from the queue
The coroutine of task B starts
Meanwhile the async operation of task A is done and the OS notifies the event loop through the underlying I/O library
Once the event loop gets control back from the coroutine of task B it takes another task from the queue
Note: Priority in the task queue is determined by how an event loop is written and can be changed from one language version to another
In our case, it is task A’
The resulting coroutine A’ now has control over the resources
Task A is now completely done and the event loop is ready to choose a new task or trigger another callback (IE: for task B)
This example isolates the execution of task A, but this is the process that essentially happens for each task, round and around until all tasks are complete (depending on how the loop was run). With this understanding, let’s now check out how this looks in code and try to understand its results:
As you can see both tasks, Stippy and Ozzy, are completed until their first pause point, after which there is a little wait before Ozzy arrives to Split and Stippy a bit later in Zadar (even though we sent out Stippy first).
For more examples check out our Github repo: https://github.com/seekandhit/asyncIO
When to use async(io)?
Asyncio excels in handling asynchronous I/O operations by allowing concurrent execution of tasks, particularly those waiting for I/O completion. It proves valuable in optimizing the performance of I/O-bound applications.
Here are some scenarios where Asyncio becomes advantageous:
I/O-bound tasks: When your application frequently awaits I/O operations, such as reading files or making HTTP requests, Asyncio can boost performance by efficiently switching to other tasks while awaiting I/O.
Network applications: Asyncio shines in building network applications like chat servers, web servers, or network clients. It adeptly manages multiple connections concurrently, enhancing application scalability.
Asynchronous APIs: Utilize Asyncio with third-party libraries or services offering asynchronous APIs. This simplifies interactions with these components, using the familiar Asyncio syntax for making requests.
Coordinating parallel tasks: For independent and concurrent tasks, Asyncio efficiently coordinates and manages them. It becomes valuable when tasks require specific order execution or resource sharing.
Keep in mind that Asyncio may not always be the ideal choice. You need to research your project and weigh the negative and positive aspects before arriving at a decision.
Once you do decide to go with Asyncio, be sure you have a solid grasp of asynchronous programming to avoid a lot of unnecessary headaches. Even when you are aware of the workings of Asyncio, there are still those annoying and common mistakes that will happen sooner or later. They can range from exiting the application too soon (don’t forget to await 😀) to getting your tasks swept up by a garbage collector. This is unavoidable, bugs will happen and they will haunt us for days. The best you can do is to read up and understand the mechanics behind it. If you need help or a reminder of how some of it works, be sure to read our article again.