Skip to content

Green Threads Proficient

⚙️ Performance & Systems
⏱️ ~2 days 📚 Prerequisites: Asyncio, Event Loop Internals

When you'd use this

Cooperative lightweight threads with gevent and greenlets.

Use lightweight cooperative threads for massive concurrency without OS-thread overhead — the idea behind gevent and async runtimes.

What you'll learn

  • What green threads are
  • Cooperative vs preemptive scheduling (tested demo)
  • gevent and monkey-patching
  • Green threads vs asyncio vs OS threads
  • When to use them

Green threads (also called lightweight threads or coroutines) are threads managed by a library rather than the OS. Thousands can run in one OS thread, switching cooperatively when one would block. The cooperative-scheduling demo here is run-verified.


The idea

The idea — a key concept in Green Threads.

An OS thread is heavyweight (its own stack, kernel scheduling). A green thread is cheap — the library schedules it in user space, switching at I/O points. You can run tens of thousands, versus maybe thousands of OS threads.

The catch: green threads are cooperative. They only switch when a task voluntarily yields (typically at an I/O call). One task that never yields blocks all the others — unlike OS threads, which the kernel can preempt at any time.


Cooperative scheduling (tested demo)

Cooperative scheduling in Green Threads — what it is and when to use it.

Generators let us model cooperative switching in pure Python — each task runs until it yields, then the scheduler moves on:

def task(name, n, log):
    for i in range(n):
        log.append(f"{name}:{i}")
        yield                       # cooperatively give up control

def run(tasks):
    log = []
    active = list(tasks)
    while active:
        nxt = []
        for t in active:
            try:
                next(t)             # advance one step
                nxt.append(t)
            except StopIteration:
                pass                # task finished
        active = nxt
    return log

log = []
run([task("A", 2, log), task("B", 3, log)])
print(log)

Output:

['A:0', 'B:0', 'A:1', 'B:1', 'B:2']

The two tasks interleave (A:0, B:0, A:1, B:1, then B alone) because each yields after one step — round-robin cooperative scheduling. This is conceptually what a green-thread library does, except it switches automatically at I/O calls rather than explicit yields.


gevent and greenlets

gevent and greenlets in Green Threads — what it is and when to use it.

The classic Python green-thread library is gevent, built on greenlet:

import gevent                       # pip install gevent
from gevent import monkey
monkey.patch_all()                  # make stdlib blocking calls cooperative

def worker(n):
    gevent.sleep(1)                 # yields to other greenlets instead of blocking
    return n * 2

jobs = [gevent.spawn(worker, i) for i in range(1000)]
gevent.joinall(jobs)                # 1000 "threads" in one OS thread

gevent snippet follows documented API

gevent isn't installed here (the cooperative-scheduler demo above is run-verified). Its trick is monkey-patching: monkey.patch_all() swaps the standard library's blocking functions (socket, time.sleep, etc.) for cooperative versions, so existing code becomes non-blocking without rewriting it as async.

Monkey-patching is invasive

monkey.patch_all() rewrites standard-library behavior globally at runtime. It's powerful (unmodified libraries become async-friendly) but can cause subtle bugs and conflicts. Do it once, at the very start of the program, before other imports — and understand you've changed how the whole process behaves.


Green threads vs the alternatives

Green threads vs the alternatives in Green Threads — what it is and when to use it.

Green threads (gevent) asyncio OS threads
Scheduling Cooperative (implicit yield) Cooperative (explicit await) Preemptive (OS)
Syntax Looks like normal blocking code async/await Normal code
Count feasible Tens of thousands Tens of thousands Thousands
Explicitness Hidden switch points Visible await points N/A

The key contrast with asyncio: gevent hides the switch points (code looks synchronous), while asyncio makes them explicit with await. Modern Python has largely standardized on asyncio for new code because explicit await makes concurrency easier to reason about — but gevent remains useful for making large existing synchronous codebases concurrent without a rewrite.


When to use them

A core question explored in Green Threads: When to use them.

  • gevent/green threads — you have a big synchronous (I/O-bound) codebase and want concurrency without rewriting it to async. Or you're on a framework built around it.
  • asyncio (Asyncio) — new I/O-bound code where explicit, readable concurrency is preferred. The modern default.
  • OS threads / futures (Futures & Executors) — when you need preemption or are integrating with blocking code you can't patch.

Practice exercises

  1. Extend the cooperative scheduler so tasks can yield a value that the scheduler collects.
  2. Add a task that never yields and show it starves the others (the cooperative pitfall).
  3. Explain what monkey.patch_all() does and why it must run before other imports.
  4. Compare the green-thread demo's interleaving to how asyncio would schedule the same tasks.
  5. Decide, for a legacy synchronous web scraper, whether gevent or an asyncio rewrite fits better, and why.

💬 Discussion

Have a question about this topic? Found an error? Share your thoughts below.