Skip to content

Process Management

The Global Interpreter Lock (GIL)

The GIL is a mutex that prevents multiple threads from executing Python bytecodes at once. This lock is necessary because Python’s memory management is not thread-safe.

  • Sharing the GIL: By default, all subinterpreters in a process share the same GIL.
  • Per-Interpreter GIL: Starting in Python 3.12, subinterpreters can be created with their own GIL, allowing true parallel execution of Python code on multiple CPU cores.

High-Level Thread Management (The GIL State API)

The simplest way to manage the GIL in C extensions is the PyGILState API. This is recommended when C code is called from an unknown thread or a thread not created by Python.

  • PyGILState_STATE PyGILState_Ensure(void): Ensures that the current thread is ready to call Python C API functions. It handles thread state creation and GIL acquisition automatically.
  • void PyGILState_Release(PyGILState_STATE state): Releases the GIL and restores the thread to its previous state.

Example Usage:

/* Ensure the GIL is held before calling any Python API */
PyGILState_STATE gstate = PyGILState_Ensure();

/* Perform Python operations */
PyObject *result = PyObject_CallObject(func, NULL);

/* Release the GIL and return to previous state */
PyGILState_Release(gstate);

Releasing the GIL for Blocking Operations

To allow other Python threads to run while a C extension performs a long-running or blocking task (like I/O or heavy computation), the GIL should be released.

  • Py_BEGIN_ALLOW_THREADS: A macro that saves the thread state and releases the GIL.
  • Py_END_ALLOW_THREADS: A macro that re-acquires the GIL and restores the thread state.

Warning

You must not call any Python C API functions between these two macros, as the thread no longer holds the GIL.

Py_BEGIN_ALLOW_THREADS
/* Perform blocking I/O or heavy C computation here */
do_heavy_work();
Py_END_ALLOW_THREADS

Low-Level Thread State API

For complex scenarios involving subinterpreters or manual thread management, you use PyThreadState objects.

  • PyThreadState* PyThreadState_New(PyInterpreterState *interp): Creates a new thread state belonging to a specific interpreter.
  • PyThreadState* PyThreadState_Swap(PyThreadState *tstate): Makes the specified thread state the "current" one for the calling OS thread.
  • void PyEval_RestoreThread(PyThreadState *tstate): Re-acquires the GIL and sets the thread state.
  • PyThreadState* PyEval_SaveThread(void): Releases the GIL and returns the current thread state.

Integration: Multi-Threading with Subinterpreters

Managing multiple interpreters requires careful handling of thread states to ensure each OS thread is attached to the correct interpreter.

Workflow for a Multi-Interpreter Thread:

  1. Creation: Create a subinterpreter using Py_NewInterpreter() or Py_NewInterpreterFromConfig(). This automatically creates a PyThreadState for the current OS thread.
  2. Execution: Run Python code. The GIL is automatically held for the new interpreter.
  3. Context Switching: To move an OS thread from Interpreter A to Interpreter B:
  4. Release GIL/ThreadState in Interpreter A using PyThreadState_Swap(NULL).
  5. Acquire/Set ThreadState for Interpreter B using PyThreadState_Swap(tstate_B).

  6. Finalization: Call Py_EndInterpreter(tstate) to clean up the subinterpreter.


Summary of Best Practices

Scenario Recommended Approach
C Callback from unknown thread Use PyGILState_Ensure() and PyGILState_Release().
I/O or Math heavy tasks Wrap the code in Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS.
Parallelism (Multi-core) Use Py_NewInterpreterFromConfig with PyInterpreterConfig_OWN_GIL.
Background C Threads Manually create a PyThreadState using PyThreadState_New() before interacting with Python.

Critical Safety Rules

  1. Deadlocks: Be cautious when acquiring multiple locks (C mutexes + the GIL) to avoid circular dependencies.
  2. Consistency: Every Ensure must have a Release, and every BEGIN_ALLOW must have an END_ALLOW.
  3. Isolation: When using subinterpreters, ensure that your C extension does not share mutable global state across interpreters unless that state is protected by its own C-level mutex.
  4. No Python API without GIL: Calling almost any Py* function without holding the GIL will result in immediate undefined behavior or a segmentation fault.