DEV Community

Cover image for Use Asynchronous Programming in Python: Don’t Block entire Thread
Muhammad Ihsan
Muhammad Ihsan

Posted on

Use Asynchronous Programming in Python: Don’t Block entire Thread

As Python is single-threaded, having a command of asynchronous programming has become mandatory to utilize that single thread efficiently. This article explores the fundamentals of asynchronous programming and elaborates with code snippets to help you fully utilize the potential of concurrency in Python.

Before diving into the actual code, let’s first understand some basic terminologies and concepts, so you can remain on track throughout the whole article.

Understanding Asynchronous Programming:

Asynchronous programming is a technique designed to handle concurrent tasks without blocking the entire thread. Whenever any program waits for something, such as DB Call/Network Request, it allows other tasks to run meanwhile, which improves overall performance.

Coroutines:

Coroutines are functions that can be paused and resumed at specific intervals. Unlike traditional functions, coroutines allow non-blocking execution by enabling tasks to pause and give control back to the event loop. This makes it possible to perform other operations while waiting for time-consuming tasks, to complete.

Asyncio is a library in Python for writing asynchronous code. The async keyword is used to define coroutines. A coroutine can contain await expressions, indicating points at which the coroutine can be paused, allowing other tasks to execute in the meantime.

Here’s a simple coroutine that can be defined using async keyword:

import asyncio

async def my_coroutine():
    print("Start")
    await asyncio.sleep(2)
    print("End")

asyncio.run(my_coroutine())
Enter fullscreen mode Exit fullscreen mode

Why using asyncio.sleep() then time.sleep()?

You might be wondering, what’s the difference between time.sleep() and asyncio.sleep(). The time.sleep() function is a synchronous method that pauses the execution of the entire program/thread for a specified duration. On the other hand, asyncio.sleep() is part of the asyncio library and is specifically designed for asynchronous code. It will not block the entire program/thread, and only pauses a coroutine for a specified duration, enabling other tasks to run concurrently.

Below is a simple example illustrating the difference:

import time
import asyncio

print("**** Synchronous sleep ****")
time.sleep(2)
print("This will be printed after 2 seconds, as entire thread was blocked")

async def coroutine_1():
    print("Pausing coroutine 1 ...")
    await asyncio.sleep(1)
    print("After a pause, resuming coroutine 1 ...")

async def coroutine_2():
    print("coroutine_2() is executed because coroutine_1() is on pause")

async def main():
    await asyncio.gather(coroutine_1(), coroutine_2())

print("**** Asynchronous sleep ****")
asyncio.run(main())

Enter fullscreen mode Exit fullscreen mode

In the synchronous example, the entire program is paused for 2 seconds during time.sleep(2), while in the asynchronous example, only the coroutine_1() is paused during asyncio.sleep(2), allowing other asynchronous task coroutine_2() to continue.

Here’s the output:

**** Synchronous sleep ****
This will be printed after 2 seconds, as entire thread was blocked

**** Asynchronous sleep ****
Pausing coroutine 1 ...
coroutine_2() is executed because coroutine_1() is on pause
After a pause, resuming coroutine 1 ...
Enter fullscreen mode Exit fullscreen mode

Event Loop:

The event loop is responsible for scheduling and executing coroutines. It ensures the tasks progress without waiting for one another.

Note: An event loop is always required if you are working with asynchronous tasks.

You can create an event loop usingnew_event_loop(), here’s the code:

import asyncio

async def any_async_task():
    await asyncio.sleep(1)

loop = asyncio.new_event_loop()
loop.run_until_complete(any_async_task())
loop.close()
Enter fullscreen mode Exit fullscreen mode

Power of asyncio.run(my_coroutine()) :

In the above paragraph, I have mentioned that an event loop is always mandatory to run asynchronous tasks, then why is our first program my_coroutine() working without an event loop? The reason is when you call asyncio.run(my_coroutine()), it automatically creates a new event loop, starts running the specified coroutine in that event loop, and then closes the event loop after completion.

Tasks:

Tasks represent the execution of a coroutine and are managed by the event loop. They allow you to run multiple coroutines concurrently.

import asyncio

async def task1():
    await asyncio.sleep(1)
    print("This will be printed after 1 second pause")

async def task2():
    print("This will be printed immediately")

async def main():
    await asyncio.gather(task1(), task2())

# Run the main function using asyncio.run
asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Here’s the output of the above program:

task2() will be executed immediately
task1() will be executed after 1 second pause
Enter fullscreen mode Exit fullscreen mode

Futures:

Futures are placeholders for the result of asynchronous operations. They allow you to retrieve the outcome of a coroutine once it is completed.

import asyncio

async def my_coroutine():
    await asyncio.sleep(1)
    return "Any Async Output"

my_future = asyncio.Future()
my_future.set_result(asyncio.run(my_coroutine()))
result = my_future.result()
print(result)
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can also achieve the above result by using the following simplified line of code:

result = asyncio.run(my_coroutine())
print(result)
Enter fullscreen mode Exit fullscreen mode

Real-life Example - Making multiple HTTP Requests:

Now, let’s make multiple HTTP Requests asynchronously:

import aiohttp, asyncio

async def fetch_data(i, url):
    print('Starting', i, url)
    async with aiohttp.ClientSession() as session:
        async with session.get(url):
            print('Finished', i, url)

async def main():
    urls = ["https://dev.to", "https://medium.com", "https://python.org"]
    async_tasks = [fetch_data(i+1, url) for i, url in enumerate(urls)]
    await asyncio.gather(*async_tasks)

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

The above program will print the following output, which shows that all 3 tasks ran almost independently without waiting for each other, and print Finished when each one is completed:

Starting 1 https://dev.to
Starting 2 https://medium.com
Starting 3 https://python.org

Finished 2 https://medium.com
Finished 3 https://python.org
Finished 1 https://dev.to
Enter fullscreen mode Exit fullscreen mode

Conclusion:

I tried to cover as many concepts related to asynchronous programming as possible. I will consistently put my efforts here on this platform. If you liked my content, then don't forget to appreciate it, it boosts my confidence.

Thanks for reading

Top comments (0)