Skip to main content

Global Interpreter Lock (GIL)

❓ What is GIL ?​

In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. The GIL prevents race conditions and ensures thread safety.

-- Wiki Python

Global Intepreter Lock (GIL) is a mutex lock that ensures only one thread has control over the Python interpreter at any given time to prevent race conditions and provide thread-safe guarantee.

Given only one thread is executed at a time, the primary outcome is that instead of true parallel computing, we get cooperative multitasking, this characteristic make it a potential performance bottleneck when performing CPU-bounded tasks or executing multi-threading programs.

πŸš„ Train of Thoughts​

Why is GIL introduced ?​

In short, CPython's memory management is not thread-safe.

Thread-unsafe code leads to unexpected behaviours and race conditions, which is often hard to spot and rationalize. By introducing the GIL, the interpreter is limited to run only one thread at a time, such that race conditions can be avoided.

It also simplifies some low-level implementation details such as memory management, call out to C extensions, etc.

What drawbacks does GIL have ?​

In hindsight, the GIL is not ideal, since it prevents multithreaded CPython programs from taking full advantage of multiprocessor systems in certain situations.

Luckily, many potentially blocking or long-running operations, such as I/O, image processing, and NumPy number crunching, happen outside the GIL. Therefore it is only in multithreaded programs that spend a lot of time inside the GIL, interpreting CPython bytecode, that the GIL becomes a bottleneck.

Can the GIL be removed ?​

Multiple implementations that tries to provide a GIL-less Python interpreter that utilizes there implementation-runtime-native multi-thread abilities does exist, e.g IronPython (.net implementation), Jython (Java implementation).

However, their implementation is often behind the canonical CPython, either on the core or community support persepectives.

And the fact the loads of features are built with the guarantees that GIL enforces, completely removing it requires insane efforts or else many Python packages and modules will break.

To eliminate the GIL, various aspects must be considered as shown below, details can be checked here.

  • Simplicity
  • Concurrency
  • Speed
  • Features
  • API compatibility
  • Prompt destruction
  • Ordered destruction
Important

As of the writing of this note (2023.11), the Steering Council has accepted the PEP 703's proposal of making GIL optional in CPython. Check the PEP for more details.

πŸ•΅οΈ Deep Dive​

Implementation < Python 3.2​

GIL was implemented in a tick-based-periodic-check approach before Python 3.2. A tick is loosely mapped to an interpreter instruction, and a periodic check function will be perforemd every 100 ticks by keeping track of the tick counts.

And a running thread will only release the GIL when it encounters I/O tasks. So the thread execution model can be simplified into a few steps:

  1. A thread acquires the GIL and starts executing while simultaneously decrement the tick count.
  2. Once 100 ticks has passed, reset tick count to 100 and forced by the interpreter to release the GIL.
  3. Run signal handlers if thread is main thread (non-main thread can't handle signals).
  4. Release GIL (context-switching).
  5. Thread scheduled by the OS acquires the GIL, return to step 1.

The problem with the model lies in the battle of threads fighting for the GIL. We can break down the possible scenarios when performing the check:

  1. Running thread is performing I/O-bounded task, it proactively releases the GIL. ➑️ All good
  2. Thread runs till the check, and is forced to release GIL.
    • If OS consider the original thread to have higher priority, the same thread reacquires the GIL. ➑️ Problematic!
    • If OS consider another thread to have higher priority, that thread acquires the GIL.

Problem gets worse when the program is running on a multi-processor system. Whenever a GIL is released, OS might falsely thinks that it is capable of running multiple task at the same time and signals other threads to wake up but still only one thread can acquire the GIL and execute. Breaking down the logic:

  1. T1 and T2 are two threads running on two differenct cores.

  2. Perform check after 100 ticks, T1 forced to release GIL.

  3. Given two cores, OS schedules T2 to execute while leaveing T1 running.

  4. T1 immediately reqcauires the GIL.

  5. T2 wakes up yet unable to acquire GIL, blocked again. Return to step 2.

    tip

    If T1 is a CPU-bound thread and T2 is I/O-bound thread, I/O-bound has a higher priority in OS scheduling yet never gets a chance to execute in above scenario, this is a priority inversion problem.

Redundant context switching and false wakeup of threads causes GIL thrashing and results in performance downgrade (up to 50% based on David Beazley's presentation), and thus reimplementation is carried out.

Further Readings

Implementation >= Python 3.2​

GIL implementation has a major reimplementation since Python 3.2 by Antoine Pitrou.

The GIL is just a boolean variable (locked) whose access is protected by a mutex (gil_mutex), and whose changes are signalled by a condition variable (gil_cond). gil_mutex is taken for short periods of time, and therefore mostly uncontended.

-- CPython/Python/ceval_gil.c

Essentially, the new implementation switched to a time-based approach and determines whether to release GIL on each interval check by utilizing a global flag _PY_GIL_DROP_REQUEST_BIT. A simplified logic is as follows:

  1. A thread acquires the GIL and starts executing code within a eval loop, where a periodic check of pending executions is performed in each turn of the eval loop.
  2. If another thread wants to acquire the GIL, it first enters a suspended state and waits for an interval of milliseconds (default to 5, available for read and modify using the Python API sys.{get,set}switchinterval()) to acquire the GIL.
  3. If executing thread willingly releases the GIL (performing I/O for instance), suspended thread acquires GIL and starts executing (back to 1.).
  4. If GIL is not released after timeout, suspended thread sets the _PY_GIL_DROP_REQUEST_BIT flag to force the executing thread to release the GIL when detected during the periodic check in the eval loop.
  5. Executing thread releases the GIL and ensures thread other than itself acquires the GIL.

Your code is not necessarily thread-safe​

Since threads within a process share the same memory space, GIL guarantees the consistency at this level, our Python code, however, is not guaranteed. The GIL does not guarantee consistency for high-level Python objects

πŸ”— References​