13.5 Asynchronous Programming with asyncio
Asynchronous programming allows a program to perform non-blocking operations, which means it can execute other tasks while waiting for slow operations (such as I/O tasks) to complete. In Python, asyncio
is a library that provides tools to write concurrent code using the async/await syntax. It enables programs to handle many tasks simultaneously without using threads or processes, making it ideal for I/O-bound and high-level structured network code.
In this section, we will explore how to use the asyncio
module for asynchronous programming, understand key concepts like event loops, tasks, coroutines, and demonstrate how asyncio
can be used for I/O-bound tasks like network requests.
13.5.1 What is Asynchronous Programming?
In asynchronous programming, instead of waiting for a task (such as reading from a file or making a network request) to complete before moving on to the next task, the program can switch to another task. This leads to more efficient execution, especially in I/O-bound tasks where the program spends a lot of time waiting.
Key characteristics of asynchronous programming:
- Non-blocking: Functions yield control when waiting, allowing other tasks to run.
- Concurrency: Multiple tasks are run concurrently, but not necessarily in parallel (they take turns).
- Efficient I/O handling: Ideal for handling multiple I/O-bound tasks like database queries, network requests, or file operations.
13.5.2 Key Concepts in asyncio
- Coroutines: Special functions defined with
async def
that can be paused and resumed. - Event Loop: The core of
asyncio
that runs asynchronous tasks and manages their execution. - Tasks: Units of work created from coroutines, managed by the event loop.
- Futures: Objects representing results that will be available in the future.
Coroutines and async def
A coroutine is a function defined with the async def
syntax. When called, it returns a coroutine object, which needs to be scheduled on the event loop to execute.
Example: Defining and Running a Simple Coroutine
import asyncio
# Define a simple coroutine
async def say_hello():
print("Hello")
await asyncio.sleep(1) # Simulate an I/O-bound task
print("World")
# Run the coroutine
asyncio.run(say_hello())
Output:
Hello
# 1 second delay
World
In this example:
async def
defines a coroutine, which is similar to a generator but can use theawait
keyword to pause execution and release control back to the event loop.await asyncio.sleep(1)
simulates a non-blocking I/O operation that waits for 1 second.
13.5.3 The Event Loop
The event loop is the core mechanism of asyncio
. It runs and schedules asynchronous tasks. You can think of it as a scheduler that decides which task runs when. The event loop repeatedly checks for tasks that are ready to run (such as those that have completed waiting for I/O) and runs them.
asyncio.run(coroutine)
: Runs the event loop until the given coroutine completes.await
: Pauses the coroutine, allowing other tasks to run, and resumes it once the awaited task is completed.
13.5.4 await
and Non-blocking I/O
The await
keyword is used to wait for the result of an asynchronous operation without blocking the rest of the program. When a coroutine calls await
, it suspends its execution and allows other tasks in the event loop to run until the awaited operation is complete.
Example: Using await
to Run Multiple Tasks Concurrently
import asyncio
async def task_1():
print("Task 1 started")
await asyncio.sleep(2) # Simulate a 2-second I/O operation
print("Task 1 completed")
async def task_2():
print("Task 2 started")
await asyncio.sleep(1) # Simulate a 1-second I/O operation
print("Task 2 completed")
# Run the tasks concurrently
async def main():
await asyncio.gather(task_1(), task_2()) # Run both tasks concurrently
asyncio.run(main())
Output:
Task 1 started
Task 2 started
Task 2 completed
Task 1 completed
In this example:
asyncio.gather()
runs bothtask_1()
andtask_2()
concurrently. Task 2 completes before Task 1 because it has a shorter sleep duration.
13.5.5 Creating and Managing Tasks
In asyncio
, you can create tasks to schedule coroutines on the event loop. A task is a wrapper around a coroutine that runs asynchronously.
asyncio.create_task()
: Schedules a coroutine as a task on the event loop, allowing it to run concurrently with other tasks.
Example: Creating and Managing Multiple Tasks
import asyncio
async def task(name, delay):
print(f"Task {name} starting")
await asyncio.sleep(delay)
print(f"Task {name} completed after {delay} seconds")
async def main():
# Create multiple tasks
task_1 = asyncio.create_task(task("A", 2))
task_2 = asyncio.create_task(task("B", 3))
print("Tasks have been created")
# Wait for both tasks to complete
await task_1
await task_2
asyncio.run(main())
Output:
Tasks have been created
Task A starting
Task B starting
Task A completed after 2 seconds
Task B completed after 3 seconds
In this example:
- Two tasks are created using
asyncio.create_task()
, allowing them to run concurrently. - The main coroutine waits for both tasks to complete using
await task_1
andawait task_2
.
13.5.6 Using asyncio.gather
to Run Coroutines Concurrently
asyncio.gather()
is a convenient way to run multiple coroutines concurrently. It schedules all the provided coroutines and waits for all of them to complete.
Example: Running Multiple Coroutines Concurrently with asyncio.gather()
import asyncio
async def download_file(filename, duration):
print(f"Start downloading {filename}")
await asyncio.sleep(duration) # Simulate a download delay
print(f"Finished downloading {filename}")
async def main():
# Download three files concurrently
await asyncio.gather(
download_file("file1.txt", 2),
download_file("file2.txt", 3),
download_file("file3.txt", 1)
)
asyncio.run(main())
Output:
Start downloading file1.txt
Start downloading file2.txt
Start downloading file3.txt
Finished downloading file3.txt
Finished downloading file1.txt
Finished downloading file2.txt
In this example:
asyncio.gather()
schedules all the coroutines (download_file()
in this case) to run concurrently, and the event loop switches between them based on the I/O delays.
13.5.7 Async I/O: aiohttp
for Asynchronous HTTP Requests
Asynchronous programming is particularly powerful for I/O-bound tasks such as making network requests. The aiohttp
library allows you to make asynchronous HTTP requests using asyncio
.
Example: Making Asynchronous HTTP Requests with aiohttp
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
print(f"Fetching {url}")
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
urls = [
'https://example.com',
'https://example.org',
'https://example.net'
]
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks) # Run all requests concurrently
for result in results:
print(result[:100]) # Print the first 100 characters of each response
asyncio.run(main())
In this example:
aiohttp.ClientSession()
is used to make asynchronous HTTP requests.asyncio.gather()
is used to run multiple HTTP requests concurrently, allowing the program to fetch multiple URLs without blocking.
13.5.8 Timeouts and Cancellation
In asyncio
, you can set timeouts for asynchronous tasks and cancel them if they take too long to complete. The asyncio.wait_for()
function allows you to set a timeout for a task, and the task.cancel()
method can be used to cancel a running task.
Example: Using Timeouts with asyncio.wait_for()
import asyncio
async def long_task():
await asyncio.sleep(5)
print("Task completed")
async def main():
try:
await asyncio.wait_for(long_task(), timeout=2) # Timeout after 2 seconds
except asyncio.TimeoutError:
print("Task timed out
")
asyncio.run(main())
Output:
Task timed out
In this example:
asyncio.wait_for()
sets a timeout of 2 seconds. Since thelong_task()
takes 5 seconds to complete, it raises aTimeoutError
after 2 seconds.
13.5.9 Async Context Managers
In asyncio
, context managers can be asynchronous. For example, network connections or file handling tasks that require setup and cleanup can use async context managers.
Example: Using Async Context Managers
import asyncio
import aiohttp
async def fetch_data(url):
async with aiohttp.ClientSession() as session: # Async context manager
async with session.get(url) as response:
return await response.text()
async def main():
result = await fetch_data('https://example.com')
print(result[:100]) # Print the first 100 characters of the response
asyncio.run(main())
In this example:
async with
is used withaiohttp.ClientSession()
to manage the lifecycle of the HTTP session asynchronously.
13.5.10 Summary
asyncio
is Python's library for writing asynchronous code that allows multiple tasks to run concurrently without blocking each other.- Coroutines: Functions defined with
async def
that can be paused and resumed usingawait
. - Event Loop: The core of
asyncio
that schedules and manages the execution of asynchronous tasks. await
: Used to pause a coroutine and wait for a result without blocking other tasks.asyncio.gather()
: Runs multiple coroutines concurrently and waits for all of them to complete.asyncio.create_task()
: Schedules a coroutine as a task to run asynchronously.
asyncio
is particularly useful for handling many I/O-bound tasks like network requests, file operations, and database access, providing an efficient and scalable way to manage asynchronous tasks in Python.