Get AI summaries of any video or article — Sign up free
Asyncio - Asynchronous programming with coroutines - Intermediate Python Programming p.26 thumbnail

Asyncio - Asynchronous programming with coroutines - Intermediate Python Programming p.26

sentdex·
4 min read

Based on sentdex's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

Asyncio enables concurrency by suspending coroutines at `await` points, allowing other tasks to run during waiting time.

Briefing

Asyncio’s core value is letting Python overlap waiting time across multiple tasks—so one slow I/O operation doesn’t freeze everything else. The tutorial contrasts synchronous execution (tasks run to completion in order) with asynchronous concurrency (tasks can be suspended and resumed), using a web-page loading example: if an image request stalls, synchronous code would hang the whole page, while asynchronous code can keep rendering other elements during the latency.

To make that concrete, the lesson starts with a deliberately CPU-ish function that finds numbers divisible by a given divisor within a range, then measures how long three calls take when run sequentially. In the synchronous version, the script runs each computation in order, so the total time is essentially the sum of the three runs. The motivation for async appears when one of the tasks becomes “slow” (in real life, that slowness usually comes from waiting on external I/O like network responses). The tutorial argues that concurrency is not parallelism: tasks don’t run simultaneously on multiple cores; instead, the event loop switches between tasks when one is waiting.

The conversion to asyncio begins with syntax: changing `def` to `async def` marks a coroutine, and importing `asyncio` provides the primitives needed for scheduling and suspension. Next comes the event loop. The code initializes it with `asyncio.get_event_loop()`, then runs the main coroutine until completion via `loop.run_until_complete(main)`, and finally closes the loop. Inside the loop, the lesson schedules work with `loop.create_task(...)`, creating separate tasks for the three coroutine calls.

A key turning point comes when the tutorial shows why “async” alone doesn’t guarantee speed. Even with coroutines and tasks, if the coroutine never hits an awaitable pause, the program still behaves like synchronous code—because there’s no moment for the event loop to switch contexts. To demonstrate the mechanism, the function adds a tiny `await asyncio.sleep(...)` when a certain condition is met (e.g., when the loop index hits a specific modulus). That artificial suspension allows other tasks to progress while one task is “waiting,” producing the expected interleaving.

The lesson also highlights trade-offs: even very small sleeps introduce overhead, and the event loop can only switch tasks at await points. It then shows how to retrieve results correctly. `run_until_complete` returns task result objects, but the actual computed values live in `result()` (e.g., `d1.results`). Finally, it covers common mistakes: forgetting to await results can cause tasks to be dispatched and then the program exits before they finish, leading to warnings like “task got destroyed.” Another frequent error is forgetting the required `async` keyword, which triggers syntax errors.

The takeaway is practical: asyncio is most effective when tasks spend time waiting on I/O, and real-world async HTTP workloads are a common fit—hence the suggestion to look at `aiohttp` for asynchronous web requests.

Cornell Notes

The tutorial explains how asyncio enables concurrency by suspending coroutines at `await` points so other tasks can run while one task waits. It starts with a synchronous function that computes divisible numbers in a range, then converts it to `async def` and schedules multiple coroutines with an event loop and `loop.create_task(...)`. The crucial lesson is that coroutines only interleave when they actually await an awaitable operation; otherwise the code still runs effectively synchronously. Adding a small `await asyncio.sleep(...)` demonstrates how task switching works, along with the overhead and trade-offs of frequent suspension. It also shows how to extract computed values from task results and warns that skipping `await` can destroy unfinished tasks when the loop ends.

Why doesn’t converting a function to `async def` automatically make it faster?

Because asyncio can only switch tasks when execution reaches an `await` point. If the coroutine performs a long CPU-bound loop without awaiting anything, there’s no waiting period for the event loop to exploit, so the tasks run in a largely sequential manner even though they’re wrapped as coroutines.

What role does the event loop play in asyncio scheduling?

The event loop is created with `asyncio.get_event_loop()`. Work is scheduled into it using `loop.create_task(coro)`, and the loop is driven with `loop.run_until_complete(main)` (or similar methods). After completion, the loop is closed with `loop.close()` to clean up resources.

How does the tutorial demonstrate real concurrency with coroutines?

It injects an artificial suspension using `await asyncio.sleep(...)` when a condition is met inside the computation loop (based on a modulus check). That awaitable pause gives the event loop a chance to run other tasks, producing an interleaving order rather than strict sequential completion.

What’s the difference between concurrency and parallelism in this context?

Concurrency here means overlapping progress by switching between tasks during waits, not running multiple tasks simultaneously on different CPU cores. The tutorial uses the shoe-tying analogy: you suspend one activity to handle another, then resume—similar to how the event loop pauses a coroutine at `await` and resumes it later.

How should results from scheduled coroutines be retrieved?

After scheduling tasks (e.g., `d1 = loop.create_task(...)`), the code must await completion to get the results. The tutorial notes that `run_until_complete` yields result objects, and computed values are accessed via `result()` (shown as `d1.results` in the example).

What common mistake causes tasks to be destroyed or warnings to appear?

Not awaiting tasks’ completion before the program exits. If the loop closes or the script ends while tasks are still pending, asyncio can emit warnings such as “task got destroyed,” indicating unfinished coroutines were discarded.

Review Questions

  1. In what specific way does the event loop decide when to switch between coroutines?
  2. What happens when you schedule multiple coroutines with `loop.create_task(...)` but never `await` their completion?
  3. Why does adding `await asyncio.sleep(...)` change the execution order compared with the purely synchronous loop?

Key Points

  1. 1

    Asyncio enables concurrency by suspending coroutines at `await` points, allowing other tasks to run during waiting time.

  2. 2

    Coroutines don’t automatically run concurrently; without awaitable operations, execution remains effectively sequential.

  3. 3

    Use `async def` to define coroutines, then schedule them with an event loop via `loop.create_task(...)`.

  4. 4

    Drive the event loop with `loop.run_until_complete(main)` and close it with `loop.close()` to avoid resource issues.

  5. 5

    Retrieve computed outputs from task results (e.g., via `result()`/`results`) and `await` tasks when you need their values.

  6. 6

    For asyncio to help, the workload must include I/O waits (network, disk, etc.); CPU-only loops won’t benefit without yielding.

  7. 7

    For real async HTTP work, `aiohttp` is suggested as a common third-party client for asyncio-based requests.

Highlights

Asyncio concurrency is about switching during waits, not parallel execution on multiple cores.
Marking a function with `async def` isn’t enough—interleaving requires actual `await` operations.
Forgetting to await pending tasks can lead to warnings like “task got destroyed” when the loop ends.
The event loop (`get_event_loop`, `run_until_complete`, `close`) is the mechanism that schedules and runs coroutines.
Adding a tiny `await asyncio.sleep(...)` inside a long loop demonstrates how task switching changes completion order.

Topics

Mentioned

  • aiohttp