Introduction
The Global Interpreter Lock (GIL) in Python is a locking mechanism that ensures that only one thread can execute Python code at a time, even on multi-core processors. Whilst this comes with some benefits, this means that it’s not easy to take advantage of multiple CPU cores for parallel processing in Python.
Recently, the Python Steering Council has indicated they intend to approve PEP 703, a proposal to create a version of CPython, the most popular Python interpreter, without the GIL. This has far-reaching implications for Python development in the future, so it’s worth understanding what’s going on.
In this post, we’ll discuss why we have the GIL in Python, why we might want to remove it, and the work that’s currently being done to get us there.
Why Do We Have the GIL?
The GIL simplifies thread management and protects against race conditions and memory corruption in Python, making it easier for developers to write concurrent code safely. It was introduced when support for threading was added to Python in the early days of the language.
Compatibility
Lots of Python packages (and the main Python interpreter, CPython) make heavy use of C extensions, which aren’t inherently thread-safe. It’s possible to get multiple threads to try and access the same resources, which can lead to extremely negative effects. The GIL made it safer to create and use C extensions, which in turn made it easier for developers in the 90s to start using Python to create software, driving adoption.
Garbage Collection and Reference Counting
The other important reason has to do with how Python handles garbage collection. Garbage collection is an automatic memory management process where the interpreter tracks and reclaims memory occupied by objects that are no longer referenced or reachable in the program. In Python, there are two main methods of garbage collection, but the most prominent is a process called reference counting.
Reference counting in Python is an efficient way to manage memory and ensure that resources are released when they are no longer in use, helping to prevent memory leaks in Python programs. Reference counting works like this:
Each object keeps track of the number of references pointing to it.
When an object's reference count drops to zero, it means there are no more references to that object in the program.
This indicates that the object is no longer needed.
Python's memory management system automatically reclaims the memory occupied by the object, effectively deleting it.
Without the GIL, multiple threads running concurrently could manipulate reference counts of objects simultaneously, leading to race conditions and memory corruption. The GIL acts as a safeguard, allowing only one thread to execute Python bytecode at a time, preventing these potential issues.
Why Do We Want to Remove the GIL?
While the GIL can make it simpler to write code without worrying too much about thread safety, it can also limit the performance gains you might expect from using multiple threads in CPU-bound tasks. For I/O-bound tasks like making HTTP requests or file operations, the GIL doesn't have as much of an impact, so you can still benefit from multithreading in those cases. So, if you’re using threads in Python to send many HTTP requests, that could improve performance, but if you’re using threads to do lots of CPU-intensive tasks, that probably won’t.
If the GIL was removed, we’d be able to get the performance benefits of threading when doing CPU-intensive tasks that we don’t currently get in CPython.
What’s Wrong With Other Approaches To Improve Concurrency?
Threading and asynchronous processing with asyncio
are both methods to improve performance, but only work for IO-bound operations. However, there are a couple of methods that work for CPU-bound operations.
Python supports multiprocessing through the multiprocessing
module, and it’s also possible to write Python-like code using Cython to improve performance. Both can improve the performance of CPU-bound operations, but they come with downsides.
Multiprocessing
Python's multiprocessing
module lets you use multiple CPU cores by creating separate processes, each with its own Python interpreter and memory space. This is effective for CPU-bound tasks as it bypasses the GIL, which operates on threads, and lets you use the multiple cores on your machine for true parallel execution.
Unfortunately, creating multiple processes has a higher resource overhead compared to threads, resulting in increased memory usage and startup time. It’s also slower and more complicated to communicate between processes than between threads. Finally, processes don’t share memory by default, making it more challenging to share data between processes and synchronize them.
Cython
Cython is a Python superset that allows you to write Python-like code with some extra annotations to improve performance. It’s a way to make your Python code run faster without having to leave the Python language completely. Cython can make your code execute more quickly by optimizing critical parts while keeping the Python language's ease and readability.
However, Cython has more of a learning curve than Python and requires knowledge of C language constructs, which makes it less accessible. Writing and maintaining Cython code can also be more complex and error-prone than writing pure Python code, particularly when you’re rewriting code originally written in Python.
So, there are a couple of methods to improve performance, but both have sizable downsides. This shows why there’s been a lot of interest in removing the GIL from Python.
Previous Attempts, and Why They Didn’t Succeed
Several projects have tried to remove the GIL from CPython in the past, with some success, e.g., the Gilectomy project. However, efforts to remove the GIL usually come with some downsides:
It’s a complex task with the potential to introduce bugs and instability to the CPython interpreter.
They may break existing Python code and libraries relying on the GIL.
They may increase memory usage.
Existing C-based Python extensions would need significant changes.
The main downside, though, is that older no-GIL projects actually made single-threaded code slower than with the GIL, meaning that if you weren’t using multithreaded to speed up CPU-bound operations, your code would perform worse than with regular CPython, which isn’t acceptable.
PEP 703, which the Python Steering Council intends to approve, will change all of this, and has gained widespread support. So let’s talk about it!
What’s PEP 703 All About? No-GIL Python, the Right Way
PEP 703 proposes a way to remove the GIL from Python but manages to avoid the performance impact on non-multithreaded code that affected other no-GIL Python projects. The Python Steering Council indicated on July 28th that they intend to approve the PEP, paving the way for no-GIL Python to enter the mainstream and eventually become the default in Python.
How Does It Work?
The main important technique to allow the GIL to be removed without affecting the performance of single-threaded code is biased reference counting.
In biased reference counting, objects accessed by a single thread have their reference counts managed more efficiently than those accessed by multiple threads, providing a performance boost for single-threaded programs where most objects are used by just one thread. This means that the performance of no-GIL Python using this technique on single-threaded operations is comparable to regular Python, whilst multi-threaded CPU-bound Python programs are much faster!
The project also makes no-GIL Python more efficient by deferring the reference counting process for top-level module objects that aren’t likely to change in a Python program, as well as designating some objects as “immortal” - objects like None
are never destroyed so don’t need to be counted. There are also changes in how memory is allocated for Python objects which makes it easier to allocate memory in a thread-safe way.
Sounds great! So the question really becomes: when will we have access to no-GIL Python?
How No-GIL Python Will Be Implemented
The Python Steering Council has described the three stages they imagine no-GIL Python following to become the default version of CPython:
Short Term: In Python version 3.13 (or possibly 3.14), they plan to introduce the no-GIL build as an experimental mode. This mode will be used to gain insights into its usage, API design, packaging, and distribution.
Mid-Term: Once they are confident about sufficient community support for the no-GIL build, it will become a supported option but not the default. A target date/version will be set for making it the default, with timing depending on factors like API compatibility and community readiness. They estimate that this phase may take a year or two, but it could be longer than this.
Long Term: The ultimate goal is for the no-GIL build to become the default, eliminating the GIL without disrupting backward compatibility. They estimate it could take up to 5 years from now to reach this stage. Throughout this process, regular evaluations will occur to ensure that progress is moving along whilst avoiding backward compatibility struggles - nobody wants to see another change like the 10-year ordeal of introducing Python 3…
Conclusion
Python is on the brink of a significant change: (potentially) removing the Global Interpreter Lock (GIL), which could greatly improve Python's ability to use multiple processor cores for better performance. The GIL, which used to play the role of a safety net, has held back Python's potential to fully utilize modern processors for CPU-bound tasks.
Unlike past attempts, PEP 703 offers a smart solution, incorporating biased reference counting, deferred reference counting, and immortal objects, amongst other techniques. It ensures that single-threaded Python programs won't slow down while giving a boost to those using multiple threads for CPU-heavy tasks.
This change will happen in three steps: first, with an experimental build mode, then as a supported option, and finally, as the default setting, possibly within the next five years! The aim is to make this transition smooth and avoid compatibility issues, bringing more power and efficiency to Python.
If you have any questions about this article, feel free to reach out to us on our Vonage Community Slack and ask us over there or by messaging us on X, previously known as Twitter.
If you’re interested in using APIs for anything to do with communications, like sending SMS, voice calls, video conferencing, 2-factor authentication, fraud prevention, and more, you can sign up for a free Vonage developer account (with free credits!).
Max is a Python Developer Advocate and Software Engineer who's interested in communications APIs, machine learning, developer experience and dance! His training is in Physics, but now he works on open-source projects and makes stuff to make developers' lives better.