Home Knowledge Base The GIL exists because CPython's memory manager uses non-atomic reference counts—Py_INCREF and Py_DECREF are plain integer increments that race without a global lock, and protecting every object individually would require one mutex per object, adding 40 bytes and a lock/unlock on every attribute access.

The GIL—Python's Global Interpreter Lock—is a single mutex that prevents more than one thread from executing CPython bytecode simultaneously, a design choice made in 1992 that solved reference-count races in 6 lines of C and has shaped Python concurrency ever since.

<svg viewBox="0 0 760 470" xmlns="http://www.w3.org/2000/svg" font-family="monospace" font-size="12">
  <rect width="760" height="470" fill="#0d1117"/>
  <!-- Title -->
  <text x="380" y="32" fill="#e6edf3" font-size="13" text-anchor="middle" font-weight="bold">CPU-Bound Throughput vs Thread Count — GIL vs Alternatives</text>
  <!-- Axes -->
  <line x1="80" y1="60" x2="80" y2="380" stroke="#8b949e" stroke-width="1.5"/>
  <line x1="80" y1="380" x2="700" y2="380" stroke="#8b949e" stroke-width="1.5"/>
  <!-- Y label -->
  <text x="20" y="225" fill="#8b949e" font-size="11" transform="rotate(-90,20,225)">Relative Throughput (1T = 1.0)</text>
  <!-- X label -->
  <text x="390" y="418" fill="#8b949e" font-size="11" text-anchor="middle">Thread / Process Count</text>
  <!-- Y ticks: 0 to 4, step 0.5. height=320, 1unit=320/4=80px -->
  <!-- y=380 is 0, y=60 is 4 -->
  <line x1="78" y1="380" x2="82" y2="380" stroke="#8b949e"/>
  <text x="70" y="384" fill="#8b949e" font-size="10" text-anchor="end">0</text>
  <line x1="78" y1="340" x2="82" y2="340" stroke="#8b949e"/>
  <text x="70" y="344" fill="#8b949e" font-size="10" text-anchor="end">0.5</text>
  <line x1="78" y1="300" x2="82" y2="300" stroke="#8b949e"/>
  <text x="70" y="304" fill="#8b949e" font-size="10" text-anchor="end">1</text>
  <line x1="78" y1="220" x2="82" y2="220" stroke="#8b949e"/>
  <text x="70" y="224" fill="#8b949e" font-size="10" text-anchor="end">2</text>
  <line x1="78" y1="140" x2="82" y2="140" stroke="#8b949e"/>
  <text x="70" y="144" fill="#8b949e" font-size="10" text-anchor="end">3</text>
  <line x1="78" y1="60" x2="82" y2="60" stroke="#8b949e"/>
  <text x="70" y="64" fill="#8b949e" font-size="10" text-anchor="end">4</text>
  <!-- X ticks: log2 scale 1,2,4,8,16. x(n) = 80 + log2(n)*155 -->
  <line x1="80" y1="378" x2="80" y2="382" stroke="#8b949e"/>
  <text x="80" y="394" fill="#8b949e" font-size="10" text-anchor="middle">1</text>
  <line x1="235" y1="378" x2="235" y2="382" stroke="#8b949e"/>
  <text x="235" y="394" fill="#8b949e" font-size="10" text-anchor="middle">2</text>
  <line x1="390" y1="378" x2="390" y2="382" stroke="#8b949e"/>
  <text x="390" y="394" fill="#8b949e" font-size="10" text-anchor="middle">4</text>
  <line x1="545" y1="378" x2="545" y2="382" stroke="#8b949e"/>
  <text x="545" y="394" fill="#8b949e" font-size="10" text-anchor="middle">8</text>
  <line x1="700" y1="378" x2="700" y2="382" stroke="#8b949e"/>
  <text x="700" y="394" fill="#8b949e" font-size="10" text-anchor="middle">16</text>
  <!-- Ideal linear scaling reference (dashed) -->
  <line x1="80" y1="300" x2="390" y2="60" stroke="#8b949e" stroke-dasharray="5,3" stroke-width="1"/>
  <text x="395" y="58" fill="#8b949e" font-size="10">ideal linear</text>
  <!-- GIL ThreadPoolExecutor (CPU-bound): flat ~1.0, slight decline -->
  <!-- throughput: 1.0, 0.98, 0.94, 0.88, 0.77 -> y=380-t*80: 300,302,305,310,318 -->
  <polyline points="80,300 235,302 390,305 545,310 700,318"
    fill="none" stroke="#f85149" stroke-width="2.5"/>
  <text x="705" y="320" fill="#f85149" font-size="10">GIL threads</text>
  <!-- ProcessPoolExecutor: near-linear to 4 cores, then plateau -->
  <!-- throughput: 1.0, 2.0, 3.8, 3.9, 3.85 -> y: 300,220,76,68,72 -->
  <polyline points="80,300 235,220 390,76 545,68 700,72"
    fill="none" stroke="#3fb950" stroke-width="2.5"/>
  <text x="705" y="70" fill="#3fb950" font-size="10">multiprocessing</text>
  <!-- free-threaded 3.13: linear but starts 28% lower at N=1 -->
  <!-- throughput: 0.78, 1.52, 2.8, 3.3, 3.4 -> y: 317,259,156,116,108 -->
  <polyline points="80,317 235,259 390,156 545,116 700,108"
    fill="none" stroke="#388bfd" stroke-width="2.5"/>
  <text x="705" y="108" fill="#388bfd" font-size="10">nogil 3.13</text>
  <!-- numpy/IO-bound (GIL released): near-linear -->
  <!-- throughput: 1.0, 1.95, 3.7, 3.85, 3.8 -> y: 300,224,84,72,76 -->
  <polyline points="80,300 235,224 390,84 545,72 700,76"
    fill="none" stroke="#e3b341" stroke-width="2"/>
  <text x="705" y="148" fill="#e3b341" font-size="10">numpy/IO</text>
  <!-- Annotation -->
  <rect x="82" y="248" width="132" height="42" fill="#161b22" rx="3"/>
  <text x="90" y="263" fill="#f85149" font-size="10">GIL: threads add</text>
  <text x="90" y="278" fill="#f85149" font-size="10">overhead, no gain</text>
  <!-- Legend -->
  <rect x="490" y="198" width="198" height="84" fill="#161b22" rx="4"/>
  <line x1="500" y1="214" x2="528" y2="214" stroke="#f85149" stroke-width="2.5"/>
  <text x="534" y="218" fill="#e6edf3" font-size="11">ThreadPool CPU-bound (GIL)</text>
  <line x1="500" y1="232" x2="528" y2="232" stroke="#3fb950" stroke-width="2.5"/>
  <text x="534" y="236" fill="#e6edf3" font-size="11">ProcessPoolExecutor</text>
  <line x1="500" y1="250" x2="528" y2="250" stroke="#388bfd" stroke-width="2.5"/>
  <text x="534" y="254" fill="#e6edf3" font-size="11">free-threaded CPython 3.13</text>
  <line x1="500" y1="268" x2="528" y2="268" stroke="#e3b341" stroke-width="2"/>
  <text x="534" y="272" fill="#e6edf3" font-size="11">numpy / I/O-bound threads</text>
</svg>

The GIL exists because CPython's memory manager uses non-atomic reference counts—Py_INCREF and Py_DECREF are plain integer increments that race without a global lock, and protecting every object individually would require one mutex per object, adding 40 bytes and a lock/unlock on every attribute access. Guido van Rossum made the trade in 1992: one lock for the entire interpreter, costing true CPU-bound thread parallelism but enabling C extensions to manipulate Python objects safely with no locking discipline of their own. The alternative—per-object fine-grained locking—was prototyped by Greg Stein in 1999 (the "free-threading" patch) and measured a 40% single-thread slowdown, which Guido rejected as unacceptable for the 99% of Python programs that are single-threaded or I/O-bound.

Since CPython 3.2, the GIL is time-based rather than bytecode-count-based: the default switch interval is 5 ms (sys.getswitchinterval()), meaning a thread holds the GIL for up to 5 ms before setting an eval_breaker flag that asks the holder to yield at the next safe point. Before 3.2, Python used a 100-bytecode check interval (sys.getcheckinterval()), which mapped to unpredictable wall-clock durations because bytecodes range from 50 ns (LOAD_FAST) to milliseconds (file I/O). At ~100 ns per average bytecode, 50,000 bytecodes execute in 5 ms—meaning a compute-heavy loop holds the GIL for 50,000 instructions before it can be preempted. The OS wakeup latency for a waiting thread is ~5 µs on Linux (futex), so each GIL handoff costs at minimum 5 µs of dead time while the waiting thread parks and re-wakes.

CPU-bound multithreaded Python programs do not scale—they regress: adding threads to a GIL-serialized workload costs 2–20% throughput per additional thread due to OS scheduling overhead and GIL contention, reaching ~77% of single-thread throughput at 16 threads. This is the canonical GIL trap: a developer expects 4× speedup from 4 threads, measures 0.94× instead, and concludes Python is slow when the correct conclusion is that the GIL has converted four threads into one thread with scheduling overhead. The escape is either multiprocessing (each process has its own GIL and its own interpreter) or numpy (which calls Py_BEGIN_ALLOW_THREADS before entering BLAS, releasing the GIL for the duration of the matrix operation).

The GIL is automatically released during all blocking I/O, time.sleep(), and any C extension that calls Py_BEGIN_ALLOW_THREADS—which means threading works correctly for I/O-bound programs up to the point where compute fractions exceed 1–5% of wall time. A web server thread doing 1 ms of Python computation and 99 ms of network wait holds the GIL for only 1% of its lifetime; 100 such threads can overlap their waits perfectly, achieving ~100× I/O parallelism with a single interpreter. This is why asyncio, gevent, and Twisted are architecturally sound despite the GIL: they are I/O multiplexers that never attempt CPU-bound parallelism and never suffer GIL contention. PyTorch similarly releases the GIL for every CUDA kernel launch, allowing Python orchestration and GPU compute to overlap.

CPython 3.13 ships an experimental free-threaded build (python3.13t) that disables the GIL via PEP 703's biased reference counting scheme, achieving near-linear CPU scaling at the cost of 28% single-thread regression caused by atomic reference counts on shared objects and per-object lock striping in dictionaries and sets. Biased reference counting assigns each object a bias-owner thread whose local refcount increments are non-atomic (~1.5 ns); remote threads that borrow the object use a separate atomic delta counter (~5 ns, CAS on x86). On the pyperformance benchmark suite, python3.13t runs 28% slower than python3.13 at 1 thread but scales to 3.2× at 4 threads and 5.8× at 8 threads—crossing the GIL version's throughput at approximately 1.3 threads. The build is opt-in and requires recompiling C extensions with Py_GIL_DISABLED support; as of 3.13, NumPy, Cython, and PyO3 have experimental compatibility.

The practical escape ladder for CPU-bound Python has four rungs in order of complexity and overhead: numpy ufuncs (GIL released, BLAS threads, zero IPC cost), concurrent.futures.ProcessPoolExecutor (full parallelism, ~2–10 ms pickle overhead per task per MB of data), Cython with nogil blocks (C-speed loops, manual memory management), and PyPy with its tracing JIT (2–10× faster single-thread, GIL still present but bytecode cost collapses). For a 1,000×1,000 float64 matrix multiply, numpy releases the GIL and achieves 3.8× throughput on 4 cores via OpenBLAS threading; ThreadPoolExecutor over pure Python achieves 1.0× (GIL-serialized); ProcessPoolExecutor achieves 3.5× with ~8 ms pickle overhead per call; python3.13t achieves 3.2× with zero IPC cost but 28% single-thread overhead.

Workload TypeThreadPoolExecutorProcessPoolExecutornumpy/IO threadspython3.13t (4T)
CPU-bound pure Python~1.0× (GIL)~3.5×N/A~3.2×
CPU-bound numpy~3.8×~3.5×~3.8×~4.0×
I/O-bound (99% wait)~80×~30×~80×~80×
Mixed (10% compute)~9×~8×~9×~10×
[GIL ACQUISITION PROTOCOL — CPython 3.12+]
Thread A running (holds GIL, up to 5 ms)
      |
      +---> eval_breaker flag set? (polled each bytecode)
      |         |
      |    YES: drop GIL → futex_signal → re-enter eval loop
      |    NO:  continue executing
      |
Thread B waiting for GIL
      |
      +---> OS futex_wait (parked, ~5 µs wakeup latency on Linux)
      |
      +---> Signal received → CAS to acquire GIL
      |         |
      |    SUCCESS → execute for up to 5 ms
      |    FAIL    → re-park (another thread won the race)
      |
Uncontended PyMutex acquire+release: ~10 ns
Contended acquire via futex:        ~100–500 ns

Read the GIL through a reference-count protocol lens rather than a parallelism barrier lens: the GIL is not primarily about preventing thread races in user code—Python's threading module provides Locks, Events, and Queues for that. It is a guarantee that the interpreter's own internal data structures (object headers, the import machinery, the small-integer cache, the frame allocator) are always consistent without every C extension author needing a PhD in concurrent programming. The 5 ms switch interval, the eval_breaker mechanism, the Py_BEGIN_ALLOW_THREADS macro, and PEP 703's biased reference counting are all attempts to answer the same question: how do you let Python objects exist safely in a concurrent world when every object's lifetime is tracked by a plain integer that someone has to protect?

gilglobal interpreter lockpython gilcpython gilpython threading gilgil python multithreadingfree-threaded pythonpython nogilpython cpu parallelismpython thread safety

Explore 500+ Semiconductor & AI Topics

From EUV lithography to CUDA optimization — search the full knowledge base or chat with our AI assistant.