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:
asyncio.gather()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
asyncdeclares asynchronous functions.awaitpauses 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.Queuesupports producer-consumer patterns.aiohttpefficiently manages async HTTP requests.