liveBook · Manning

Global interpreter lock

As explained in Refactoring to Rust

The Python Global Interpreter Lock is a synchronization mechanism in CPython that restricts execution of Python bytecode to one thread at a time within a process.

The global interpreter lock (GIL) lets only one thread at a time run code through the Python interpreter within a process. It protects interpreter resources from simultaneous access, but it also limits parallel execution of Python work across threads. A thread pool alone therefore may not speed up a CPU-heavy Python task. section 8.5 section 8.6

A lock shared by threads

Think of the GIL as a single hall pass: one thread holds it while using interpreter resources; another must wait its turn. This makes the safety trade-off easier to see. Adding more threads does not add more passes. section 8.5

The lock matters explicitly when Rust interacts with Python. Most Python operations require it to be held. PyO3’s `Python::with_gil` supplies a handle for operations such as creating a `PyDict`, importing modules, and running Python code. A `Python` parameter on a PyO3 extension module likewise indicates that the GIL is held. section 6.5 section 6.4

use criterion::Bencher;
use pyo3::prelude::*;
use pyo3::types::PyDict;

fn bench_py(b: &mut Bencher, code: &str, input: &str) {
  Python::with_gil(|py| {
    let locals = PyDict::new(py);

    locals.set_item("json", py.import("json").unwrap()).unwrap();
    locals
      .set_item("rust_json", py.import("rust_json").unwrap())
      .unwrap();
    locals.set_item("INPUT", input).unwrap();

    b.iter(|| black_box(py.run(code, None, Some(&locals)).unwrap()));
  });
}

The benchmark helper acquires the GIL before creating Python locals and executing Python code. Notice how the `py` handle is passed to `PyDict::new` and used for imports and `py.run`. section 6.5

For CPU-heavy work, a targeted Rust refactor offers a different route from adding Python threads. Keep the Python-facing function and move the computation into Rust; PyO3’s `py.allow_threads` releases the GIL around that work so other threads can run. The work inside the closure must be thread-safe. This is a change to a bottleneck, not a reason to rewrite an entire application. section 8.6

Listing 8.11 lib.rs: Adding a thread-safe function
#[pyfunction]
fn mandelbrot_fast(
    py: Python<'_>,
    size: u32,
    path: &str,
    range_x0: f64,
    range_y0: f64,
    range_x1: f64,
    range_y1: f64,
) {
    py.allow_threads(|| mandelbrot_func(size, path, range_x0, range_y0,
range_x1, range_y1))  #1
}

#[pymodule]
fn mandelbrot(_py: Python, m: &PyModule) -> PyResult<()> {
   ...
       m.add_function(wrap_pyfunction!(mandelbrot_fast, m)?)?;

    Ok(())
}

The Mandelbrot wrapper places the Rust computation inside `py.allow_threads`. Compare that deliberate release with `Python::with_gil`: one permits other threads to run during Rust work, while the other obtains the lock for Python operations. section 8.6 section 6.5

How Refactoring to Rust covers it

How deep it goes
Explains the lock through a shared-resource analogy, then shows PyO3 calls that acquire or release it; does not detail interpreter scheduling.
Examples
Uses a hall-pass analogy, a Python benchmark run through Rust, and a Mandelbrot wrapper using `py.allow_threads`.
What it assumes
Builds on Python threading and thread pools while introducing the relevant PyO3 lock-handling calls through examples.
What you can do after
Apply it via PyO3 by acquiring the GIL for Python operations and releasing it around thread-safe Rust computation.
How it connects
Connects the GIL to Python threads, shared-memory safety, thread pools, Rust extensions, and PyO3.

Also explained in

Compare how 5 books explain global interpreter lock →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Refactoring to Rust. The section links point to the parts of the book it draws on.