Async Basics
What is Async I/O?
Asyncio allows concurrent execution using a single thread with cooperative multitasking. Great for I/O-bound tasks with many concurrent connections.
import asyncio
# Define coroutine
def greet(name):
return f'Hello, {name}!'
# Async function (coroutine)
async def greet_async(name):
await asyncio.sleep(1) # Non-blocking sleep
return f'Hello, {name}!'
# Run coroutine
result = greet_async('Alice') # Creates coroutine object
result = asyncio.run(greet_async('Alice')) # Actually runs it
async/await
import asyncio
import time
# Synchronous
def sync_download(url):
time.sleep(2) # Blocks for 2 seconds
return f'Data from {url}'
# Asynchronous
async def async_download(url):
await asyncio.sleep(2) # Yields control for 2 seconds
return f'Data from {url}'
# Running async functions
async def main():
# Sequential (not what we want)
result1 = await async_download('url1') # 2 seconds
result2 = await async_download('url2') # 2 seconds
# Total: 4 seconds
# Concurrent (what we want)
result1, result2 = await asyncio.gather(
async_download('url1'),
async_download('url2')
)
# Total: 2 seconds (parallel)
asyncio.run(main())
When to Use Asyncio
# ✅ Use asyncio for:
# - Network I/O (HTTP requests, websockets)
# - Database queries
# - File I/O (with aiofiles)
# - Many concurrent connections
# ❌ Don't use asyncio for:
# - CPU-bound tasks (use multiprocessing)
# - Simple scripts (use threading)
# - Blocking I/O (won't help)
Asyncio Features
Tasks and Gathering
import asyncio
async def fetch_data(url, delay):
print(f'Fetching {url}...')
await asyncio.sleep(delay)
print(f'Done {url}')
return f'Data from {url}'
async def main():
# Create tasks
task1 = asyncio.create_task(fetch_data('url1', 2))
task2 = asyncio.create_task(fetch_data('url2', 1))
# Gather results
results = await asyncio.gather(task1, task2)
print(results) # ['Data from url1', 'Data from url2']
asyncio.run(main())
Timeouts
import asyncio
async def slow_operation():
await asyncio.sleep(10)
return 'Done'
async def main():
try:
result = await asyncio.wait_for(slow_operation(), timeout=2)
except asyncio.TimeoutError:
print('Operation timed out')
asyncio.run(main())
Async Iterators
class AsyncCounter:
def __init__(self, stop):
self.stop = stop
self.current = 0
def __aiter__(self):
return self
async def __anext__(self):
if self.current >= self.stop:
raise StopAsyncIteration
self.current += 1
await asyncio.sleep(0.1)
return self.current
async def main():
async for num in AsyncCounter(5):
print(num) # 1, 2, 3, 4, 5
asyncio.run(main())
Async Context Managers
import asyncio
class AsyncDatabase:
async def __aenter__(self):
print('Connecting...')
await asyncio.sleep(1)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print('Disconnecting...')
await asyncio.sleep(1)
return False
async def query(self, sql):
await asyncio.sleep(0.5)
return f'Result of {sql}'
async def main():
async with AsyncDatabase() as db:
result = await db.query('SELECT * FROM users')
print(result)
asyncio.run(main())
Exception Handling
import asyncio
async def risky_operation():
await asyncio.sleep(1)
raise ValueError('Something went wrong')
async def main():
tasks = [
asyncio.create_task(risky_operation()),
asyncio.create_task(asyncio.sleep(2))
]
# gather with return_exceptions
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
print(f'Error: {result}')
else:
print(f'Result: {result}')
asyncio.run(main())
Asyncio vs Threading
# Asyncio:
# - Single thread
# - Cooperative multitasking
# - More efficient for many I/O operations
# - No race conditions (no shared state)
# - Requires async/await syntax
# Threading:
# - Multiple threads
# - Preemptive multitasking
# - Better for blocking I/O
# - Can have race conditions
# - Simpler syntax (no async/await)
# Rule of thumb:
# - Many network connections -> asyncio
# - Few blocking operations -> threading
# - CPU-bound -> multiprocessing