Is it finally time to remove the Python GIL?

Is it finally time to remove the Python GIL?

Powerful, flexible, and programmer-friendly, Python is widely used for everything from web development to machine learning. By the two most-cited measures, Python has even surpassed the likes of Java and C to become the most popular programming language of all. After years of soaring popularity, Python might well seem unstoppable.

But Python faces at least one big obstacle to its future growth as a programming language. It’s called the GIL, the global interpreter lock, and Python developers have been trying to remove it from the default implementation of Python for decades now.

Although the GIL serves a critical purpose, namely ensuring thread safety, it also creates a serious bottleneck for multithreaded programs. In short, the GIL prevents Python from taking full advantage of multiprocessor systems. For Python to be a first-class language for concurrent programming, many believe the GIL has to go.

So far, attempts to remove the GIL have failed. But a new wave of efforts is mounting to make the GIL a thing of the past, and make Python even more fully equipped to meet programming requirements of the future.

Why Python has a GIL

Strictly speaking, the global interpreter lock isn’t part of Python in the abstract. It’s a component of the most commonly used Python implementation, CPython, which is maintained by the Python Software Foundation.

The GIL ensures thread safety in CPython by allowing only one running thread at a time to execute Python bytecode. CPython’s memory management systems aren’t thread-safe, so the GIL is used to serialize access to objects and memory to prevent race conditions. If CPython didn’t have a GIL, it would have to handle concurrency and race conditions in some other way.

What makes the GIL such a problem? For one, it prevents true multithreading in the CPython interpreter. That makes a whole class of code accelerations—optimizations that are readily available in other programming languages—far harder to implement in Python.

Most developers work around the GIL in one form or another. The multiprocessing module, for instance, makes it possible to run concurrent instances of the Python interpreter (each on its own physical thread) and share work between them. However, because sharing data between Python instances incurs a lot of overhead, multiprocessing only works well for certain classes of problems.

Another workaround is to use Python extensions, typically written in C. These run outside the Python interpreter, so the processing they perform isn’t beholden to the GIL. The problem is this only holds true as long as the work doesn’t involve Python objects, just C code and C data structures. So, like multiprocessing, C extensions only solve a small class of problems.

As Python’s popularity grows, so does the embarrassment over a deficit like the GIL in the language. And so various efforts, past and present, have been launched to do away with the GIL.

Getting rid of the GIL

The problem, as you might guess, is that getting rid of the GIL is far easier said than done. The GIL serves an important purpose. Its replacement must not only ensure thread safety but fulfill a number of other requirements besides.

Among the many goals a GIL replacement must meet, these are the most crucial:

  • Enable concurrency. The big payoff for having a GIL-less Python is true concurrency in the language. Replacing the GIL with another mechanism that doesn’t enable concurrency is not progress.

  • Not slow down single-threaded programs. Any GIL replacement that causes single-threaded programs to run slower is a net loss, because the vast majority of Python software is single-threaded.

  • Not break backwards compatibility. Existing Python software not only must run as fast as before, but should behave as expected.

  • Not incur a major maintenance cost. Python’s development team doesn’t have infinite resources or manpower. A GIL-less Python would need to be at least as easy to maintain as the existing interpreter.

Given the high bar for a GIL substitute, it’s no wonder all previous attempts to remove the GIL have either stalled or come to naught.

Pablo Galindo, one of the five members of the Python Steering Council that determines the direction of Python’s development, does believe that removing the GIL is a realistic goal for Python, “but also a very tricky one.”

“The question is not really if it is possible (we know it is certainly possible),” Galindo said in an email interview. “The question is what is the real price? and if we, as a community, want to pay that price. This too is a complicated matter, because the price to pay is also not distributed equally.”

The price of removing the GIL is paid not only by Python’s core developers, but by all of the developers who use Python, and by those who maintain packages for the Python language as well.

Previous efforts to remove the GIL

Getting rid of the GIL isn’t a new idea. Previous efforts to de-GIL Python offer examples of the difficulties Galindo talks about.

The first formal attempts to ditch the GIL date as far back as 1996, when Python was at version 1.4. Greg Stein created a patch to remove the GIL, chiefly as an experiment. It worked, but single-threaded programs took a significant performance hit. Not only was the patch not adopted, but the experience made it clear that removing the GIL was difficult. It would come at a whopping developmental cost.

In the last few years, as Python’s popularity soared, more GIL removal projects have come to the fore. One widely discussed effort was Larry Hastings’s Gilectomy project, a fork of Python employing several significant changes to reference counting and other internal mechanisms. The Gilectomy showed some promise, but broke most of the existing CPython API, and even the most valiant work on Hastings’s part couldn’t make the Gilectomy as performant as CPython.

Several other projects involved forking Python and rewriting it to better support parallelism. PyParallel, one such project, removed the GIL as a limitation to better parallelism without actually removing the GIL. PyParallel added a new module, parallel, that allowed objects to communicate with each other via the TCP stack. While PyParallel successfully circumvented the GIL, the approach had limitations. For one, parallel code had to communicate via the TCP stack (slow), instead of by a shared memory mechanism (fast). PyParallel hasn’t been updated since 2016.

PyPy, the JIT-compiling Python alternative, not only has a GIL of its own, but also a GIL-removal project. The goal of STM (Software Transactional Memory) was to speed up multiple threads in parallel in PyPy, but here too the cost was a significant hit to single-threaded performance, anywhere from 20% to 2x slower. The STM branch of PyPy is no longer under active development either.

Current efforts to remove the GIL

The bad track record for previous attempts to remove the GIL has spurred some new thinking about the way forward. Maybe the best approach is not to remove the GIL, but—as PyParallel attempted—to make it less of an obstacle to parallelism by sidestepping it, then offer that functionality to the average Python developer.

In theory, Python modules like multiprocessing and third-party projects like Dask already do this. One spins up multiple, distinct copies of the interpreter, splits a task among them, and serializes object data between them if needed. But multiprocessing comes with a lot of overhead, and third-party projects are just that—third-party offerings, not native components built into Python.

A few Python proposals are in the works to improve this situation. None of them by themselves constitutes a solution; all are still just proposals. But collectively, they hint at the direction Python is moving in.

Removing the GIL with subinterpreters

One project, PEP 684, is the “per-interpreter GIL” project. The idea is to have multiple Python interpreters, each with its own GIL, running in a single process. In fact Python has supported doing this since version 1.5, but interpreters in the same process have always shared too much global state to achieve true parallelism. PEP 684 moves as much of the shared state as possible into each interpreter, so they can run side by side with minimal interdependency.

But one big problem with this approach is how to share Python objects between interpreters. Sharing raw data, like streams of bytes, isn’t difficult, but it’s also not very useful. Sharing rich Python objects is far more useful, but also far more difficult. Nevertheless, any plan to allow true concurrency must include a way to share Python objects.

Galindo says the subinterpreter approach (as it’s also called) is a prime candidate for working around the GIL, and for providing a strategy for handling Python objects across interpreters. As Galindo put it in an email to me:

One of the attractive prospects of multiple interpreters is that it may be possible to channel objects between these interpreters in the same memory space, without the need to marshal them across processes. This may also help with some aspects of the copy-on-write problem that CPython has with multiple interpreters, but this is still to be seen, as we are lacking a complete implementation with a fully-defined surface API.

In other words, there’s a lot more work to be done on CPython’s internals before a per-interpreter GIL can happen.

Another proposal, originally raised in 2017, goes hand in hand with PEP 684. PEP 554 exposes multiple-interpreter functionality for the average Python user as part of the standard library, instead of requiring them to write a C extension. This way, as multiple interpreters become more genuinely useful, Python developers will have a standard way to work with them.

Other ideas for removing the GIL

Yet another proposal, raised in January 2023 and currently under active debate, provides a way for developers to work on a GIL-less Python side by side with existing Python.

PEP 703 adds a build option to CPython to allow compiling the interpreter without a GIL. The default would still be to include the GIL, but Python developers could work on removing the GIL as part of CPython development directly, instead of in a separate project. In time, and with enough work, the GIL-less version of Python could become the default build mode.

But this approach comes with multiple downsides. A major one is a larger maintenance cost, not only for CPython but also for extensions that might break because of assumptions about CPython’s internals. Further, as with all previous attempts to remove the GIL, the PEP 703 changes would result in a performance hit for single-threaded programs.

Whether Python makes the GIL optional, adopts subinterpreters, or takes another approach, the long history of efforts and experimentation shows there is no easy way to remove the GIL—not without huge development costs or setting Python back in other ways. But as data sets grow ever larger, and AI, machine learning, and other data processing workloads demand greater parallelism, finding an answer to the GIL will be a key element to making Python a language for the future and not just the present.

Add a Comment