Back to Curriculum

Async Programming with asyncio

📚 Lesson 12 of 20 ⏱️ 55 min

Async Programming with asyncio

55 min

Async programming allows you to write concurrent code that can handle multiple operations simultaneously without the complexity of threads. This is particularly powerful for I/O-bound operations like network requests, file operations, and database queries where your program spends time waiting for external resources. Async programming can dramatically improve performance for applications that perform many I/O operations.

The `asyncio` library provides the infrastructure for writing single-threaded concurrent code using coroutines. Unlike threads, asyncio uses cooperative multitasking where coroutines voluntarily yield control, making it more efficient and easier to reason about than preemptive threading. This eliminates many of the race conditions and synchronization issues common with threads.

Async functions are defined with `async def` and use `await` to call other async functions. The `await` keyword pauses the function until the awaited operation completes, but unlike blocking I/O, it allows other coroutines to run in the meantime. This is the key to asyncio's efficiency—while one coroutine waits for I/O, others can execute.

The event loop is the core of asyncio—it manages and schedules coroutines, handles I/O operations, and coordinates concurrent execution. Understanding how the event loop works helps you write efficient async code and debug async-related issues. The event loop runs until all tasks are complete or until you stop it explicitly.

`asyncio.gather()` allows you to run multiple coroutines concurrently and wait for all of them to complete. This is perfect for parallel I/O operations where you want to fetch multiple resources simultaneously. It's much faster than awaiting each operation sequentially, as all operations run concurrently rather than one after another.

Error handling in async code requires special attention. Exceptions in coroutines need to be caught with try-except, and `asyncio.gather()` has options for handling exceptions from individual tasks. Understanding these patterns is crucial for robust async applications. You can use `return_exceptions=True` to collect exceptions instead of stopping on the first error.

Key Concepts

  • Async programming enables concurrent I/O operations without threads.
  • Coroutines are functions defined with async def that can be paused and resumed.
  • await pauses execution until the awaited operation completes.
  • The event loop manages and schedules coroutines for concurrent execution.
  • asyncio.gather() runs multiple coroutines concurrently.

Learning Objectives

Master

  • Writing async functions with async def and await
  • Using asyncio.run() to execute async code
  • Running multiple coroutines concurrently with asyncio.gather()
  • Handling errors and exceptions in async code

Develop

  • Understanding concurrent programming concepts
  • Recognizing when async programming is beneficial
  • Designing async applications with proper error handling

Tips

  • Use async/await for I/O-bound operations, not CPU-bound tasks (use multiprocessing instead).
  • Use asyncio.gather() to run multiple async operations concurrently for better performance.
  • Always use asyncio.run() to run the main async function in modern Python.
  • Be careful not to mix async and sync code incorrectly—use proper async libraries.

Common Pitfalls

  • Using async for CPU-bound tasks (use multiprocessing instead for parallelism).
  • Forgetting to await async function calls, getting coroutine objects instead of results.
  • Blocking the event loop with synchronous I/O operations, defeating the purpose of async.
  • Not properly handling exceptions in async code, causing silent failures.

Summary

  • Async programming enables efficient concurrent I/O operations.
  • Coroutines use async def and await for non-blocking operations.
  • The event loop manages concurrent execution of coroutines.
  • asyncio.gather() runs multiple operations concurrently for better performance.

Exercise

Create an async function that simulates a network request.

import asyncio\nimport time\n\nasync def fetch_data(delay):\n  print(f"Starting fetch with {delay}s delay...")\n  await asyncio.sleep(delay)\n  return f"Data fetched after {delay} seconds"\n\nasync def main():\n  tasks = [fetch_data(1), fetch_data(2), fetch_data(3)]\n  results = await asyncio.gather(*tasks)\n  for result in results:\n    print(result)\n\nasyncio.run(main())

Exercise Tips

  • Compare sequential vs concurrent execution to see the performance difference.
  • Add error handling with try-except to handle exceptions in individual tasks.
  • Use asyncio.create_task() to run coroutines concurrently without immediately waiting.
  • Experiment with asyncio.wait_for() to add timeouts to async operations.

Code Editor

Output