liveBook · Manning

Single-threaded execution model

As explained in Python Concurrency with asyncio

A single-threaded concurrency model is a concurrency design in which one thread interleaves progress among multiple tasks.

A single-threaded concurrency model uses one thread to interleave progress among multiple tasks. With asyncio, non-blocking I/O lets web requests, database queries, and network connections proceed in tandem without additional threads. section 2.0

Concurrency does not imply parallel CPU execution. CPU-bound coroutines still run sequentially on the event-loop thread, and can block unrelated tasks that use await. section 2.7.1

Use the model with these practical boundaries:

  • Prefer it for long-running I/O operations that can yield control. section 2.0
  • Do not expect multiple CPU-heavy tasks to finish faster merely because they are asyncio tasks; use multiprocessing with a process pool when CPU work must coexist with async code. section 2.7.1
  • Treat shared state carefully: one executing Python line at a time avoids some races, but suspension at await can still produce inconsistent outcomes. section 11.1
  • Use synchronization primitives when coroutines coordinate access, limit requests, protect resource pools, or wait for notifications. section 11.0

Two CPU-bound tasks

Creating two tasks for a large counting loop still runs the first loop and then the second, so total runtime is approximately the sum of both calls. A delay coroutine also waits because the CPU-bound work blocks the event loop. section 2.7.1

A single-threaded race

A counter increment without suspension completes safely in one thread, but reading shared state, awaiting, and writing it later allows another coroutine to intervene. An asyncio lock can serialize a message broadcast and user disconnection. section 11.1 section 11.2

Listing 2.18 Attempting to run CPU-bound code concurrently
import asyncio
from util import delay
 
 
@async_timed()
async def cpu_bound_work() -> int:
    counter = 0
    for i in range(100000000):
        counter = counter + 1
    return counter
 
 
@async_timed()
async def main():
    task_one = asyncio.create_task(cpu_bound_work())
    task_two = asyncio.create_task(cpu_bound_work())
    await task_one
    await task_two
 
 
asyncio.run(main())

Runnable asyncio code demonstrates that creating tasks does not make CPU-bound loops execute in parallel. section 2.7.1

How Python Concurrency with asyncio covers it

How deep it goes
Explains task scheduling, CPU-bound blocking, and await-related races, while deferring process-pool execution details.
Examples
Uses CPU-bound counting loops, delay tasks, shared counters, socket dictionaries, echo servers, and asyncio locks.
What it assumes
Assumes basic concurrency context while teaching asyncio coroutines, tasks, event loops, and synchronization progressively.
What you can do after
Apply it via asyncio to overlap I/O tasks, recognize event-loop blocking, and protect suspended shared-state workflows.
How it connects
Connects asyncio with event loops, coroutines, tasks, non-blocking I/O, race conditions, locks, and multiprocessing.

Also explained in

Compare how 2 books explain single-threaded execution model →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Python Concurrency with asyncio. The section links point to the parts of the book it draws on.