Python Cooperative Scheduling — Core Concepts

Cooperative vs preemptive scheduling

Operating systems use preemptive scheduling: the kernel interrupts running processes at any time to give other processes a turn. The process has no say in when it’s paused.

Python’s asyncio uses cooperative scheduling: each coroutine runs until it explicitly yields control via await. The event loop never interrupts a running coroutine. The coroutine decides when to pause.

PreemptiveCooperative
OS threads, processesasyncio, generators, trio
Kernel interrupts at any timeTask yields at await
Needs locks for shared dataShared data safe between awaits
Can’t hog the CPU (kernel prevents it)A blocked task freezes everything
Complex race conditionsSimpler reasoning about state

How the event loop works

The asyncio event loop is a single-threaded scheduler:

  1. Pick the next ready task from the queue
  2. Run it until it hits await
  3. The await registers a callback (I/O ready, timer expired, etc.)
  4. Return to step 1
  5. When a callback fires, its task becomes ready again

Between any two await points, your code has exclusive access to all shared state. This is why asyncio programs rarely need locks — atomicity comes from the cooperative design.

The yield points

Every await is a potential context switch. Between awaits, no other task can run:

async def transfer(source, dest, amount):
    # No other task runs during these lines
    balance = source.balance
    source.balance = balance - amount
    dest.balance += amount
    # ^ All three operations are "atomic" because there's no await

Compare with the dangerous version:

async def transfer_dangerous(source, dest, amount):
    balance = source.balance
    await some_io_operation()  # ← another task could modify source.balance here!
    source.balance = balance - amount
    dest.balance += amount

The await creates a window where other tasks can run and modify shared state. This is the fundamental reasoning tool for async Python.

The starvation problem

If a coroutine does CPU-heavy work without awaiting, it blocks the entire event loop:

async def cpu_hog():
    # This blocks ALL other tasks for the entire computation
    result = sum(i * i for i in range(10_000_000))
    return result

Solutions:

  • Break work into chunks with periodic await asyncio.sleep(0) to yield
  • Offload to a thread with asyncio.to_thread() for CPU-bound work
  • Use a process pool for heavy computation

Cooperative scheduling beyond asyncio

Python has multiple cooperative scheduling systems:

  • Generators (yield) — the original cooperative multitasking in Python
  • asyncio (await) — standardized async I/O scheduling
  • trio — structured concurrency with stricter yield guarantees
  • greenlet/gevent — monkey-patched cooperative threading

All share the core principle: the running code decides when to give up control.

Common misconception

“Cooperative scheduling is slower than preemptive.” For I/O-bound workloads, cooperative scheduling is often faster because context switches only happen at meaningful points (I/O boundaries), not at arbitrary times. There’s no kernel overhead for switching, no TLB flushes, no register saves. An asyncio context switch costs roughly 1-5 microseconds; an OS thread switch costs 10-50 microseconds.

The one thing to remember: cooperative scheduling means tasks yield control voluntarily at await points. Between awaits, your code runs atomically. This makes async Python simpler to reason about than threaded code, but requires discipline to avoid blocking the event loop with CPU-intensive work.

pythonadvancedconcurrency

See Also

  • Python Actor Model Why treating each piece of your program like a person with their own mailbox makes concurrency way less scary.
  • Python Aiocache Caching aiocache remembers expensive answers so your async Python app doesn't waste time asking the same question twice.
  • Python Aiofiles Async Io aiofiles lets your async Python program read and write files without freezing — because normal file operations secretly block everything.
  • Python Aiohttp Understand Aiohttp through an everyday analogy so Python behavior feels intuitive, not random.
  • Python Anyio Portability AnyIO lets your async Python code work with any async library — write once, run on asyncio or Trio without changes.