Python Asynchronous Programming – asyncio and await

What the asynchronous programming brings to the table is that one can make a program carry out an extensive list of things codelessly concurrent, without blocking execution on other operations. This is particularly helpful when handling I/O-bound tasks such as:

  • Network requests
  • Fileen actions
  • Problems of database queries
  • Wear input on GUI apps

Instead of waiting for one task to complete and then moving on to the next one, asynchronous programming allows the program to continue executing while waiting for other tasks to finish.

1. Synchronous vs Asynchronous Programming

Before diving into asyncio, let’s understand how synchronous programming differs from asynchronous programming.

1.1 Synchronous Execution (Blocking)

In a traditional synchronous program, operations execute one after another in a blocking manner.

Example of Synchronous Code:

import time

def task1():
    print("Task 1 started")
    time.sleep(3)  # Simulating a time-consuming operation
    print("Task 1 completed")

def task2():
    print("Task 2 started")
    time.sleep(2)  # Simulating another time-consuming operation
    print("Task 2 completed")

def main():
    task1()
    task2()

main()

Output:

Task 1 started
(Task 1 takes 3 seconds to complete)
Task 1 completed
Task 2 started
(Task 2 takes 2 seconds to complete)
Task 2 completed

Here, task2 starts only after task1 is fully completed. This is inefficient because the program remains idle while waiting for time.sleep() to complete.

1.2 Asynchronous Execution (Non-Blocking)

Asynchronous programming allows tasks to run concurrently, improving efficiency.

Example of Asynchronous Code:

import asyncio

async def task1():
    print("Task 1 started")
    await asyncio.sleep(3)  # Simulating a time-consuming operation
    print("Task 1 completed")

async def task2():
    print("Task 2 started")
    await asyncio.sleep(2)  # Simulating another time-consuming operation
    print("Task 2 completed")

async def main():
    await asyncio.gather(task1(), task2())  # Runs both tasks concurrently

asyncio.run(main())

Output:

Task 1 started
Task 2 started
(Task 2 completes first after 2 seconds)
Task 2 completed
(Task 1 completes after 3 seconds)
Task 1 completed

Here, both tasks start at the same time, and the program does not block execution while waiting.

2. Understanding asyncio and await

Python offers the library asyncio for asynchronously written code, based on mechanisms of event looping that effectively implement several tasks on one flow line.

2.1 Keyword of async

The async keyword defines an asynchronous function, which is otherwise referred to as a coroutine. It simply indicates to Python that the function has asynchronous operations inside.

async def my_function():
    print("This is an async function")

2.2 await Keyword

The await keyword pauses the execution of an async function until the awaited operation completes. It must be used inside an async function.

import asyncio

async def delayed_message():
    print("Message will appear after 2 seconds...")
    await asyncio.sleep(2)  # Non-blocking sleep
    print("Hello, world!")

asyncio.run(delayed_message())

Output:

Message will appear after 2 seconds...
(Waits for 2 seconds)
Hello, world!

The await keyword ensures that the program does not block execution and can perform other tasks during the wait time.

3. Running Multiple Asynchronous Tasks

Instead of executing tasks one by one, we can run multiple coroutines concurrently using:

  1. asyncio.gather()
  2. asyncio.create_task()

3.1 Running Multiple Coroutines with asyncio.gather()

asyncio.gather() takes multiple coroutines and runs them concurrently.

import asyncio

async def task1():
    await asyncio.sleep(3)
    print("Task 1 completed")

async def task2():
    await asyncio.sleep(2)
    print("Task 2 completed")

async def main():
    await asyncio.gather(task1(), task2())  # Run both tasks in parallel

asyncio.run(main())

Output:

Task 2 completed
Task 1 completed

(Task 2 finishes first because it has a shorter sleep duration.)

3.2 Using asyncio.create_task()

Another way to run multiple tasks is by using asyncio.create_task(). This method schedules tasks and allows the program to proceed without waiting.

import asyncio

async def task(name, duration):
    print(f"Starting {name}")
    await asyncio.sleep(duration)
    print(f"Finished {name}")

async def main():
    t1 = asyncio.create_task(task("Task 1", 3))
    t2 = asyncio.create_task(task("Task 2", 2))

    await t1  # Waits for Task 1
    await t2  # Waits for Task 2

asyncio.run(main())

Output:

Starting Task 1
Starting Task 2
Finished Task 2
Finished Task 1

(Task 2 finishes first because it has a shorter sleep duration.)

4. Using asyncio.Queue for Producer-Consumer Patterns

asyncio.Queue allows communication between coroutines in a producer-consumer pattern.

import asyncio

async def producer(queue):
    for i in range(3):
        await asyncio.sleep(1)
        await queue.put(i)  # Add items to the queue
        print(f"Produced {i}")

async def consumer(queue):
    while True:
        item = await queue.get()
        print(f"Consumed {item}")
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    prod_task = asyncio.create_task(producer(queue))
    cons_task = asyncio.create_task(consumer(queue))

    await prod_task
    await queue.join()  # Ensures all tasks are processed
    cons_task.cancel()  # Stop the consumer

asyncio.run(main())

Output:

Produced 0
Consumed 0
Produced 1
Consumed 1
Produced 2
Consumed 2

5. Handling Exceptions in Async Code

We can use try-except blocks to catch errors inside coroutines.

import asyncio

async def risky_task():
    await asyncio.sleep(1)
    raise ValueError("Something went wrong!")

async def main():
    try:
        await risky_task()
    except ValueError as e:
        print(f"Caught an error: {e}")

asyncio.run(main())

Output:

Caught an error: Something went wrong!

6. Async HTTP Requests with aiohttp

For making non-blocking HTTP requests, we use aiohttp.

import asyncio
import aiohttp

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    url = "https://www.example.com"
    html = await fetch(url)
    print(html[:100])  # Print first 100 characters

asyncio.run(main())

Conclusion

  • async declares asynchronous functions.
  • await pauses execution until the awaited task completes.
  • asyncio.run() begins an event loop and runs coroutines.
  • asyncio.gather() runs several coroutines in parallel.
  • asyncio.create_task() schedules coroutines for running.
  • asyncio.Queue supports producer-consumer patterns.
  • aiohttp efficiently manages async HTTP requests.