Skip to content

Threads, processes & the GIL

Senior

What the GIL does and doesn't prevent, threading vs multiprocessing vs asyncio for mixed workloads, and lock-ordering deadlocks.

Created 2 September 2026 · Updated 23 October 2026

3 concepts · detail hidden

  1. 01The GIL — why it exists and what it does and doesn't prevent

    The GIL (Global Interpreter Lock) is a single mutex (a lock that only one thread can hold at a time) inside CPython — the standard, reference implementation of Python that most people mean when they just say “Python” — that ensures only one OS thread executes Python bytecode (the compiled, interpreter-level instructions your source code turns into) at any given instant, even on a multi-core machine. It exists mainly to protect CPython’s reference counting (the mechanism by which every object tracks how many things point to it, so it can be freed the instant nothing references it anymore) from race conditions: if two threads could increment/decrement a refcount simultaneously without coordination, counts could get corrupted, causing memory leaks or premature frees (a “use after free” bug, where memory is freed while something still points to it — one of the nastiest classes of bug in systems programming). This does not mean Python is single-threaded in the sense of “only one thread can exist” — you can absolutely create multiple threads, and they genuinely help for I/O-bound work (waiting on network calls, disk reads, etc.), because the GIL is released while a thread is blocked waiting on I/O, letting another thread run Python code in the meantime. What the GIL prevents is true CPU-bound parallelism via threads — two threads doing heavy computation can’t actually run Python bytecode on two cores simultaneously; for that you need multiprocessing (separate OS processes, each with its own interpreter and GIL) or C extensions (like numpy) that release the GIL while doing their own C-level computation. Bonus/advanced context: PEP 703, implemented as the experimental “free-threaded” CPython build (available from 3.13 onward, built with --disable-gil), removes the GIL entirely — but this isn’t a free lunch: without the GIL, CPython needs other, more granular ways to keep refcounting safe across threads, which adds overhead to single-threaded performance (the case that used to be uncontended and fast) and the ecosystem of C extensions needs updating to be safe without the GIL’s implicit protection.

    #c-the-gil-why-it-exists-and-what-it-does-and-doesnt-prevent
  2. 02Threading vs multiprocessing vs asyncio — architecting a mixed I/O + CPU workload

    This scenario deliberately mixes two very different kinds of work, and the right architecture uses a different concurrency tool for each. The 50 API calls are I/O-bound (the CPU is idle, just waiting on network responses) — asyncio (or, as a simpler alternative, a thread pool) is the right tool here, since the GIL is released while waiting on I/O, so many concurrent requests can genuinely overlap their waiting time even though only one runs Python code at any instant. The pandas transformation step is CPU-bound (actual computation, not waiting) — this is exactly the case the GIL prevents threads from parallelizing, so a ProcessPoolExecutor (spinning up separate OS processes, each with its own interpreter and therefore its own GIL) is needed to actually use multiple CPU cores. It’s also worth deliberately bounding concurrency for the I/O side — backpressure via a semaphore, limiting how many API calls are in flight at once — rather than firing all 50 requests simultaneously, both to respect the external APIs’ rate limits and to avoid overwhelming your own process with too much simultaneous work.

    #c-threading-vs-multiprocessing-vs-asyncio-architecting-a-mixed
  3. 03Deadlocks with `threading.Lock`, and preventing them by lock ordering

    A deadlock occurs when two (or more) threads are each waiting on a resource the other holds, so neither can ever proceed — the classic version: thread A acquires lock1 then tries to acquire lock2; thread B, running concurrently, has already acquired lock2 and is now trying to acquire lock1 — each is stuck waiting forever for a lock the other is holding and won’t release until it gets the lock it’s waiting for. A common but weak “fix” is adding a timeout to lock acquisition so a stuck thread eventually gives up and raises an error instead of hanging forever — but this doesn’t actually prevent the deadlock, it just converts an infinite hang into a (still disruptive) failure after some delay, and doesn’t address the underlying design flaw. The real architectural fix is lock ordering: establishing and enforcing a single, consistent global order in which locks are always acquired across the entire codebase (e.g., “always acquire lock1 before lock2, everywhere, no exceptions”) — if every thread acquires multiple locks in the same relative order, the circular “each waiting on what the other holds” situation structurally cannot arise. An even stronger fix, where feasible, is redesigning so no code path ever needs to hold more than one lock at a time in the first place, eliminating the possibility entirely rather than just disciplining around it.

    #c-deadlocks-with-threadinglock-and-preventing-them-by-lock-ord