Asyncio Is Not Parallelism. You may have heard that Python asyncio... | by Peter Xie | Towards Data Science

Let’s start with a perfect concurrent example #1.
Async function say_after is an example from Python official documentation. It prints something ‘what’ after sleeping ‘delay’ seconds.
In the main function, we create two tasks of say_after, one says ‘hello’ after 1 second and the other say ‘world’ after 2 seconds. Run it and we see it takes 2 seconds in total because both tasks run concurrently. Perfect!
But are they parallel? Let’s play around with more examples to understand the difference between concurrency and parallelism.
Example #2:
We replace the main function as follows.
As you can see, we add a print after creating tasks to check if tasks start immediately after creation, and add an async sleep 0.5 seconds after that in the main function. Note that main itself is a task (coroutine exactly).
Below is the output:
First, it still takes 2 seconds in total, no change. It runs main and other two say_after tasks concurrently. The async sleep in the main is not blocking.
Second, “Before delay — after creating tasks” is printed before starting say_after tasks. Yes! Created tasks do not start immediately after creation, instead, it is scheduled to run in a so-called event loop. They start only when the main task is waiting, i.e. await asyncio.sleep(0.5) in this case. And to my understanding, you cannot control the sequence of execution, i.e. priorities, of the tasks.
Example #3:
In this example, we replace the asyncio.sleep with time.sleep which waits with blocking in main, and see when say_after tasks start.
See that the total is now 2.5 seconds. And task1 starts 0.5 second after it is created. It is clear that tasks are not parallel, i.e. execute at the same time.
Example #4:
You may argue that asyncio.sleep should be used instead of time.sleep with asyncio programming. How about the main task is doing something and causes the delay?
In this example we replace time.sleep with a loop to add about 1 second delay in the main task.
You see that we got a similar result. say_after tasks are delayed to start and the total time becomes 3 seconds.
Example #5:
If a task starts, does it guarantee it ends in expected time? No!Let’s see this example below.
We have asyncio.sleep(0.1) in line #7 to allow task1 and task2 to start, but add time.sleep(3) in line #8 to block for 3 seconds afterwards.Here is the output:
You see both tasks start immediately in line #3 and #4, but do not ‘say’ after the expected 1 seconds or 2 seconds, instead ‘say’ (end) after 3 seconds.
The reason is that when say_after is awaiting for 1 / 2 seconds, the event loop goes back to main task and blocks there for 3 seconds before it can loop back to say_after tasks to continue.
You can find the full demo file here.
Asynicio tries the best to be concurrent but it is not parallel.
You cannot control the start nor the end of a task.
You may control the start if you await the task immediately after it is created as follows, but it becomes synchronous programming then, which makes no sense for asynchronous purpose. Note even that is not 100 percent guaranteed and think yourself.
task1 = asyncio.create_task(say_after(1, ‘hello’))await task1
So if you are developing a timing-sensitive app, avoid using asyncio (coroutine event loop broadly). The cause of this limitation is that the event loop uses only one thread to schedule multiple tasks.
So what’s the solution for parallelism? Threading.
This is the equivalent code for example #5 which has 3 seconds blocking sleep in the main function.
And the output is as follows.
See that both task1 and task2 start immediately and ends in expected 1 and 2 seconds.
You can use multiprocessing as well to make use of multiple CPU cores.
Lastly, I am not saying you should not use the event loop, which is great in handling network volumes. But it depends on your need.