Use Asynchronous Programming in Python: Don’t Block entire Thread
Tags: python, programming, tutorial, computerscience
Published: 2024-01-09 18:53:16

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:

```python
import asyncio

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

asyncio.run(my_coroutine())
```

### 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:

```python
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())

```

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:

```yaml
**** 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 ...
```

### **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 using`new_event_loop()`, here’s the code:

```python
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()
```

### 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.

```python
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())
```

Here’s the output of the above program:

```vhdl
task2() will be executed immediately
task1() will be executed after 1 second pause
```

### **Futures:**

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

```python
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)
```

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

```python
result = asyncio.run(my_coroutine())
print(result)
```

## Real-life Example - Making multiple HTTP Requests:

Now, let’s make multiple HTTP Requests asynchronously:

```python
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())
```

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:

```sql
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
```

## **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