﻿---
title: "Understanding Python's GIL: The Problems It Solves and the Limits It Imposes"
date: 2025-06-20
excerpt: The GIL is not just why Python threads can't use multiple cores. It is CPython's engineering trade-off among refcounting, the object model, and C extensions.
tags:
  - Python
  - GIL
  - Atomicity
  - Thread
  - Concurrency
cover: https://assets.vluv.space/cover/Lang/Python/GIL.webp
updated: 2026-07-08 21:17:39
lang: en
i18n:
  cn: /GIL
  translation: 2
---

Because of the GIL, Python's multithreading cannot make effective use of multiple CPU cores. But if you treat the GIL as nothing more than a performance liability, it is hard to understand why CPython has kept it for so long, or why "removing the GIL" touches the whole interpreter and its extension ecosystem.

This article is for people who already write Python and know what threads and locks are. It focuses on: what problem the GIL was meant to solve, why "just give every object its own lock" sounds reasonable but is often worse, and why the GIL makes C extensions easier to integrate.

## What the GIL Is

The **GIL (Global Interpreter Lock)** is exactly what the name says: a global lock over the interpreter. At any given moment, only one thread can hold the GIL; only the thread holding the GIL may execute Python bytecode[^1] and directly manipulate interpreter state[^2].

```mermaid
sequenceDiagram
    autonumber
    participant T1 as Thread 1
    participant GIL
    participant T2 as Thread 2

    T1->>GIL: acquire()
    activate GIL
    Note right of T1: GIL acquired
    T1->>T1: Execute bytecode

    T2->>GIL: acquire()
    Note left of T2: Waiting for GIL

    T1->>GIL: release()
    deactivate GIL
    Note right of T1: GIL released

    activate GIL
    Note left of T2: GIL acquired
    T2->>T2: Execute bytecode
    T2->>GIL: release()
    deactivate GIL
    Note left of T2: GIL released
```

## Why the GIL Exists

### Simplifying Memory Management

CPython's memory management has long relied on **reference counting**. Every object header carries a counter, `ob_refcnt`, recording how many places currently hold a reference to the object.

- When a new variable points to it: `Py_INCREF` bumps the count by **+1**
- When a reference goes away (variable destroyed, reassigned, etc.): `Py_DECREF` decrements it by **-1**
- When the count reaches **0**: the object is reclaimed immediately (memory freed)

```python
import sys

a = []
print(sys.getrefcount(a))  # usually one higher than you expect, because the getrefcount call itself holds a reference
```

This scheme is relatively simple to implement. But in standard CPython, `Py_INCREF` and `Py_DECREF` are plain C macros that just execute `{c} op->ob_refcnt++` or `--`. Increment and decrement in C are not atomic, so under concurrent modification in a multithreaded environment the count can drift, with two possible outcomes:

* Count too high: the object can never be freed, leaking memory.
* Count too low: the object may be freed while still in use, leading to dangling pointers or even segfaults.

If concurrent refcount updates are unsafe, why not give each object its own lock, or make `Py_INCREF` atomic?

Intuitively that seems more sophisticated than one big global lock: the locks are finer-grained, so concurrency looks higher. But inside a runtime like CPython, where almost everything is an object, it is not that simple.

Start with one key fact: **refcount operations are extremely frequent, and their critical sections are extremely short.**

Plenty of ordinary code triggers `INCREF` or `DECREF` under the hood:

* Variable assignment
* Function arguments and return values
* Container reads and writes
* Creation and destruction of temporary objects
* Saving and cleaning up objects during exception propagation

In other words, per-object locking (or any finer-grained locking scheme) has to reckon with at least the following.

<link rel="stylesheet" href="/css/optional/accordion.css">

<div class="accordion">
<details class="accordion-item" name="accordion-gil-1">
  <summary>1. High lock overhead</summary>

A refcount update is essentially just changing an integer in the object header, but acquiring and releasing a per-object lock requires:

* Atomicity: operations protected by the lock must be atomic, i.e., not interruptible by another thread
* Memory Visibility: one thread's changes to an object must become visible to other threads in time
* Cache Coherence: on multi-core CPUs each core may have its own cache, and per-object locks must keep the object state consistent across cores
* Deadlock avoidance and reentrancy

</details>
<details class="accordion-item" name="accordion-gil-1">
  <summary>2. Object bloat and worse cache hit rates</summary>

Putting a lock on every object bloats the object header, which hits the huge number of small objects: small ints, strings, tuples.

Consequences:

* Higher memory usage
* Fewer hot objects fit in CPU cache
* Slower hot paths

</details>
<details class="accordion-item" name="accordion-gil-1">
  <summary>3. Multi-object operations get complicated</summary>

Python operations usually touch several objects, e.g., assignment or `{py} list.append(x)`.

Problems:

* Multi-object locking needs a strict ordering, or deadlocks come easily
* Many code paths would need rewriting
* C extensions would also have to follow the locking protocol

Per-object locks are not just slow; they significantly increase implementation complexity.

</details>
</div>

CPython chose one global lock instead, which brings:

* Refcount updates are naturally serialized, no per-object locks needed
* Many interpreter-internal structures can assume "only one thread is mutating right now"
* Lock acquire/release cost is amortized over larger execution slices instead of once per object operation

## Why the GIL Makes C Extensions Easier to Integrate

This part is often dismissed in one sentence, but it is actually one of the key reasons the GIL has survived so long.

CPython's C API has long been built on one important constraint:

> As long as extension code holds the GIL, it may treat most interpreter-internal state as "exclusively owned by the current thread."

This means a C extension working with a `PyObject*` can usually do the following directly, without adding another layer of per-object locks:

* Read and write reference counts
* Access the internals of lists, dicts, tuples, and other objects
* Create new objects, raise exceptions, call Python APIs
* Use the many legacy C API macros and fast paths

This dramatically lowers the mental burden on extension authors. For many extensions, the rule almost boils down to one sentence:

> Before touching a Python object, make sure you hold the GIL.

This default assumption matters a lot, because many C extension authors are not concurrent-runtime experts. They may be great at numerical computing, image processing, database drivers, or system interfaces, but they do not want to re-derive object lifetimes and lock ordering on every Python C API call.

Without the GIL, under a truly fine-grained concurrency model, C extensions could no longer assume "once I enter the Python API, I'm in an approximately single-threaded world."

That immediately raises a string of extra requirements:

* Before every operation on a Python object, consider whether another thread may be modifying it concurrently
* Legacy interfaces like borrowed references, container iterators, and cached pointers become more fragile
* If an extension keeps raw pointers into a Python object's internals, their lifetimes must be re-examined
* Many old extensions would need added locks, changed data structures, and adjusted API usage

In other words, removing the GIL is not just "the interpreter tweaks itself"; **the entire CPython C API contract has to be tightened or rewritten.**

### Concurrency in C Extensions

The GIL does not force C extensions to run serially the whole time. What it really provides is a simple boundary:

* **Hold the GIL while operating on Python objects**
* **Voluntarily release the GIL during pure C computation or blocking I/O**

That is why many extensions can integrate safely while still running in parallel on the hot paths. The typical pattern looks like this:

```c
Py_BEGIN_ALLOW_THREADS
do_blocking_io_or_native_compute();
Py_END_ALLOW_THREADS
```

The benefit of this model is safe by default, opt out as needed.

Extension authors do not have to write everything as fine-grained thread-safe code from day one; they only need to explicitly release the GIL during the phases that never touch Python objects.

## How the GIL Affects Python Multithreading

### CPU-bound Python threads cannot truly run in parallel

If two threads are both running pure-Python CPU-bound code, they take turns holding the GIL instead of executing bytecode simultaneously on two cores.

That is why so many people find that "adding threads to Python didn't make it any faster."

### I/O-bound workloads are still a good fit for threads

If threads spend most of their time waiting on the network, disk, or other blocking I/O, threads are still valuable, because the waiting phases usually give up the GIL. What multithreading buys you here is not single-core compute, but the **overlap of waiting time**.

### Native extensions can sidestep the limit

If the expensive part lives in a native extension written in C, C++, Rust, or Fortran, and that code releases the GIL during computation, then multiple threads really can run in parallel underneath.

CPU-bound code often has a C extension implementation available, so in practice you frequently do not need to worry about the GIL at all.

> [!WARNING]
>
> The GIL does not mean "Python multithreaded code is automatically thread-safe." The GIL protects interpreter-internal state, not your business variables.
>
> Read-modify-write logic like `remain_count -= 1` can still interleave across threads and produce wrong results.

## How to Work Around the GIL

Three common approaches:

* **Multiprocessing**: `multiprocessing` gives each process its own interpreter instance and its own GIL, well suited to CPU-bound tasks.
* **Native extensions**: move hot paths into C, C++, Rust, Cython, or any implementation that can release the GIL.
* **Switch implementations, or watch the free-threaded track**: PEP 703 is pushing a no-GIL / free-threaded CPython, but that is not as simple as "delete one lock"; it is a systematic overhaul of the object model, the refcounting strategy, and C extension compatibility.

## Summary

Overall, the global lock is an engineering compromise among performance, memory, and complexity: **it sacrifices parallelism for CPU-bound multithreading in exchange for lower interpreter implementation complexity, cheaper single-threaded paths, and ecosystem compatibility.**

## Read More

[Inside The Python Virtual Machine | 深入理解 Python 虚拟机](https://nanguage.gitbook.io/inside-python-vm-cn)

[^1]: Python bytecode is a low-level, platform-independent intermediate code produced by compiling Python source (.py), living in `.pyc` files or in memory. It is executed on a virtual machine by the [CPython interpreter](https://www.google.com/url?sa=i&source=web&rct=j&url=https://github.com/python/cpython/blob/main/Python/bytecodes.c&ved=2ahUKEwjhpJ3r1NuTAxXqI0QIHfyjMDUQy_kOegQIARAB&opi=89978449&cd&psig=AOvVaw0WwKyy027gNarNeiUzIsQw&ust=1775648653619000) and speeds up program loading. Developers can disassemble bytecode with the built-in [dis module](https://www.google.com/url?sa=i&source=web&rct=j&url=https://docs.python.org/zh-tw/3/library/dis.html&ved=2ahUKEwjhpJ3r1NuTAxXqI0QIHfyjMDUQy_kOegQIARAC&opi=89978449&cd&psig=AOvVaw0WwKyy027gNarNeiUzIsQw&ust=1775648653619000) to study how a program runs.
[^2]: The collection of all runtime data structures and execution context maintained by the Python interpreter (usually CPython) while running. It includes the object system (ints, strings, etc.), memory management (refcounting and GC), the call stack and frames, thread and interpreter state, bytecode execution state (instructions and the operand stack), the Global Interpreter Lock (GIL), modules and namespaces, and exception handling. See the [PyInterpreterState struct](https://github.com/python/cpython/blob/3.7/Include/pystate.h#L113) in the Python 3.7 source.
