Skip to main content

Asynchronous Programming

About

An asynchronous program executes one step at a time, similar to a synchronous program. However, the system doesn't wait for each step to complete before proceeding to the next. As the program is able to move on to future execution steps while the previous step is running elsewhere, this also means that the program knows what to do when a previous step does finish running.

Asynchronous programming in Python is often implemented using async and await keywords. These allow the program to execute code without blocking operations, making it efficient for I/O-bound tasks like reading/writing files, fetching data from APIs, or managing multiple network connections.

Few Important Components

Before we dive deeper into async in Python and asyncio, these components or concepts might be helpful

Coroutines

Python Documentation

Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points.

Coroutines define the operations to be performed asynchronously, and rely on event loop to actually execute them.

Event Loop

Python Documentation

The event loop is the core of every asyncio application. Event loops run asynchronous tasks and callbacks, perform network IO operations, and run subprocesses.

Think of event loop as the "traffic controller" or "orchestrator" that schedules and manages the execution of tasks and coroutines. It works by:

  1. Schedules coroutines (created with async def)
  2. Runs the coroutine until it reaches an await point, at which point it pauses and executes other tasks
  3. When the awaited operation completes, the event loop resumes the paused coroutine

Futures

Python Documentation

A Future represents an eventual result of an asynchronous operation. Not thread-safe.

Future is an awaitable object. Coroutines can await on Future objects until they either have a result or an exception set, or until they are cancelled. A Future can be awaited multiple times and the result is same.

Think of futures as a placeholders for eventual results. A Future is either in a pending state or finished, it allows the program to "wait" for results without blocking

Tasks

Python Documentation

A Future-like object that runs a Python coroutine. Not thread-safe.

Tasks are used to run coroutines in event loops. If a coroutine awaits on a Future, the Task suspends the execution of the coroutine and waits for the completion of the Future. When the Future is done, the execution of the wrapped coroutine resumes.

Think of tasks as the "coroutine manager", they are higher-level constructs that wrap coroutines

Putting The Pieces Together

  1. Define Coroutines: Coroutines (async def) describe the logic for asynchronous operations
  2. Schedule Coroutines in the Event Loop: The event loop orchestrates the execution of coroutines
  3. Wrap Coroutines into Tasks: Coroutines are wrapped into Tasks for execution by the event loop
  4. Manage Results with Futures: The event loop tracks Futures to determine the state of operations (e.g., pending or finished)
  5. Execute Concurrently: The event loop switches between coroutines at await points, ensuring no operation blocks the program

Example Implementation

import asyncio

async def fetch_data():
print("Fetching data...")
await asyncio.sleep(2)
print("Data fetched!")

async def process_data():
print("Processing data...")
await asyncio.sleep(1)
print("Data processed!")

async def main():
# Schedule tasks (coroutines wrapped into tasks)
fetch_task = asyncio.create_task(fetch_data())
process_task = asyncio.create_task(process_data())

# Await tasks (retrieve their Futures' results)
await fetch_task
await process_task

print("Tasks completed")

# Start the event loop
asyncio.run(main())

Execution Steps

  1. The event loop starts
  2. fetch_data and process_data are schedules as Tasks
  3. Both tasks run concurrently:
    • fetch_data pauses at await asyncio.sleep(2)
    • The event loop switches to process_data, which pauses at await asyncio.sleep(1)
  4. When process_data finishes (as the waiting time is faster than that of fetch_data), the event loop resumes fetch_data
  5. Both tasks are completed

Output

Note that the first 2 lines will be output at the same time

Fetching data...
Processing data...
Data processed!
Data fetched!
Tasks completed