Hi Everyone,
Ever wondered why your Python program feels slow when making multiple API calls or database queries? The reason is Python’s synchronous nature, and the solution is concurrency. In this post, we’ll explore how asyncio can make your Python code faster and more efficient.
Let’s discuss concurrency in Python. Many software developers have confusion between concurrency and parallelism.
Before moving forward, I’d assume that you’ve a basic understanding of Python and its execution flow.
Before moving to asyncio, let’s learn about concurrency and the difference between concurrency and parallelism.
If you already know about this, you can skip to asyncio.
Concurrency vs Parallelism
Parallelism: multiple tasks are executed in parallel, i.e., at the same time
Concurrency: Only 1 task is executed at a time. But if 1 task is waiting for some output or some external input, the control will move to the next task for the time being.
Example: A practical yet very simple example can be:
Let’s say we have a cafe.
Parallelism: 2 people are working in that cafe, so at a time, 2 orders can be processed.
Concurrency: Only 1 person is working in a cafe. But when he gets multiple orders, like a Pizza and a coffee. He’ll put the Pizza in the oven, and instead of waiting for the Pizza to be baked, he’ll move towards preparing coffee.
Concurrency keeps a thread busy by reducing its idle time.
Although Python is not a single-threaded programming language. But GIL (Global Interpreter Lock) prevents multiple threads from executing Python bytecode simultaneously.
It means that one server thread will cater to a request.
Why Default Python Execution Can Be Slow
By default, Python is synchronous. It means that next line of code will be executed only after the previous code is executed.
If the previous line of code is taking 7 seconds, the thread will wait for that code to be executed and will move to next line once the previous line of code is executed.
Now, let’s understand why it can be a problem in some cases:
import time
def function1():
time.sleep(2)
print("function1")
def function2():
time.sleep(4)
print("function 2")
def function3():
time.sleep(1)
print("function 3")
def main():
function1()
function2()
function3()
if __name__ == "__main__":
main()
If you run the above code:
- It will take approx. 7 seconds to execute
- Function execution order will look like this:
- function1() —> function2() —> function3()
We can assume that the order of the function execution doesn’t matter and all the functions are independent of each other.
Let’s look into the control flow more closely.
START > function1() —> function2() ——> function3() → END
With asyncio, you can reduce this time to 4 seconds.
Let’s look at the code and control flow.
import asyncio
async def function1():
await asyncio.sleep(2)
print("function1")
async def function2():
await asyncio.sleep(4)
print("function2")
async def function3():
await asyncio.sleep(1)
print("function3")
async def main():
batch = await asyncio.gather(
function1(),
function2(),
function3()
)
print(batch)
if __name__ == "__main__":
asyncio.run(main())
Output
function3
function1
function2
Flow
The above code completes in ~4 seconds because the tasks run concurrently instead of sequentially. Asyncio doesn’t wait for function1() to finish before starting others.
Since function3() finished in 1 second, function3 is printed 1st.
After that, function1 is printed, and finally, after 4 seconds, function2 is printed.
Note that during these 4 seconds, only 1 thread is involved, and it was doing 1 job at a time. It was just switching between the tasks to save time.
Real-World Use Cases of Asyncio
Let’s admit you won’t use the above exact code in real life. But you’ll use asyncio where:
The order of the function execution doesn’t matter, and all the functions are independent of each other
Here are some practical use cases:
- Multiple API calls: You need to call multiple APIs, and all the APIs are independent of each other
- Database queries: You have multiple database queries. Usually, DB queries take some time to return the result. You can execute other queries in the meantime.
- File Connections: You need to fetch multiple files from the SFTP server. Since you need to fetch all the files, the order of the files doesn’t matter.
Asyncio: Best Use Cases and Limitations
We know when you use asyncio, but we should NOT use asyncio in CPU-intensive tasks like Machine Learning or Image Processing.
Asyncio works best for I/O-bound tasks like API calls, DB queries, and file operations. For CPU-bound tasks, consider multiprocessing or libraries like concurrent.futures
Now, let’s summarize the article. We learned about
- Difference between concurrency, parallelism, and the difference b/w them.
- A simple use-case to realize the need for concurrency.
- Simple program to understand asyncio
- Real-world use cases to use concurrency
- When to use asyncio and when not to.
With this, let’s wrap our discussion. If you have any suggestions for improvements, feedback or doubts, please feel free to comment. I’ll be more than happy to respond.
Let’s meet next time. Till then, keep coding!
