TRT
Sign in
easyExpertconcept

What does the CPython GIL actually protect, when is it released, why isn't `x += 1` thread-safe under the GIL, and how does PEP 703 (Python 3.13t/3.14t).

Detail `ob_refcnt` and `pymalloc` protection, `eval_breaker` 5-ms switching, `Py_BEGIN_ALLOW_THREADS`, and PEP 703's trio: Immortal Objects, Biased Reference Counting, and `mimalloc`.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

In standard CPython, ob_refcnt++ and ob_refcnt-- are plain non-atomic C integer operations (obj->ob_refcnt++). To prevent two OS threads from racing on ob_refcnt or pymalloc's global pools, CPython requires a thread to hold the Global Interpreter Lock (GIL) before executing Python bytecode. A waiting thread sleeps on a condition variable for sys.getswitchinterval() (5 ms); if the holder hasn't released the GIL (e.g., during blocking I/O or inside a C/Rust extension wrapped in Py_BEGIN_ALLOW_THREADS), the waiting thread sets the atomic eval_breaker bit, forcing the holder to drop the GIL at its next bytecode branch! In PEP 703 (Free-Threaded Python, python3.13t / 3.14t), the GIL is removed via three breakthroughs: (1) Immortal Objects (PEP 683): Singletons (None, True, small ints, interned strings) have a special high bit set in ob_refcnt so threads never modify their refcount (zero cache-line bouncing!). (2) Biased Reference Counting: Each object splits its refcount into ob_ref_local (modified with fast non-atomic instructions by the owning thread that created the object!) and ob_ref_shared (modified with LOCK XADD atomics only by other threads). (3) Per-Object Locks (PyMutex) and thread-safe mimalloc heaps.

Under standard GIL CPython, at most 1 OS thread executes Python bytecode at any instant (while arbitrarily many threads may execute C code inside Py_BEGIN_ALLOW_THREADS); under PEP 703 Biased Reference Counting, the owning thread modifies ob_ref_local non-atomically while non-owning threads modify ob_ref_shared atomically. Because only the single owning thread matching obj->ob_tid is permitted to read/write ob_ref_local during normal execution and all other threads use atomic compare-and-swap on ob_ref_shared, no data race can occur on reference counts even without a global lock.

Replacing every Py_INCREF with an atomic LOCK XADD across threads would slow single-threaded Python down by ~25% due to CPU cache-line bouncing; Immortalization + Biased Reference Counting keeps single-threaded overhead to just ~5–8% while unlocking 5x–7x multi-core scaling on 8 threads! In production systems (CPython Python/ceval_gil.c, Python 3.13t/3.14t (--disable-gil), NumPy/PyTorch Py_BEGIN_ALLOW_THREADS C kernels, and PyMutex 1-byte parking locks (Include/internal/pycore_lock.h).), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how the Global Interpreter Lock (GIL) and PEP 703 Biased Reference Counting operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Pure Python Loop on 8 Threads (With GIL), NumPy / C Extension on 8 Threads (ALLOW_THREADS)) with concrete numbers.
  • Identifies real-world failure modes (Assuming the GIL makes Python application state (self.balance += amt) thread-safe) and explains how production systems mitigate them.

Common mistakes

  • Treating the Global Interpreter Lock (GIL) and PEP 703 Biased Reference Counting as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Spawning 16 CPU-bound pure-Python threads on a GIL-enabled interpreter).
  • Forgetting boundary conditions and error recovery (Modifying a global singleton's refcount from 32 threads in a C extension).

This answer walks

Practice follow-ups

  1. 01In PEP 703 Free-Threaded Python, how are built-in `dict` and `list` operations kept safe from C-level memory corruption when two threads access them concurrently (`PyMutex` per-object locks)?
  2. 02Why can decreasing `sys.setswitchinterval(0.00001)` hurt CPU throughput while improving I/O thread responsiveness?

Not started

Sign in to save your learning progress.

Sign in to save