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
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
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:
- Schedules coroutines (created with
async def
) - Runs the coroutine until it reaches an
await
point, at which point it pauses and executes other tasks - When the awaited operation completes, the event loop resumes the paused coroutine
Futures
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
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
- Define Coroutines: Coroutines (
async def
) describe the logic for asynchronous operations - Schedule Coroutines in the Event Loop: The event loop orchestrates the execution of coroutines
- Wrap Coroutines into Tasks: Coroutines are wrapped into Tasks for execution by the event loop
- Manage Results with Futures: The event loop tracks Futures to determine the state of operations (e.g., pending or finished)
- 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
- The event loop starts
fetch_data
andprocess_data
are schedules as Tasks- Both tasks run concurrently:
fetch_data
pauses at awaitasyncio.sleep(2)
- The event loop switches to
process_data
, which pauses atawait asyncio.sleep(1)
- When
process_data
finishes (as the waiting time is faster than that offetch_data
), the event loop resumesfetch_data
- 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