What is the Python Global Interpreter Lock (GIL)?
Understanding the purpose of Global Interpreter Lock in Python and how it impacts multi-threading
Introduction
In one of my recent articles, I discussed about a few fundamental concepts in programming namely concurrency, parallelism, multi-threading and multi-processing and how they can be implemented in Python.
One of the most controversial topics in this context, is definitely Python’s Global Interpreter Lock that essentially protects the interpreter which -in Python- is not thread-safe.
In today’s article we will revisit threading and multi-processing and introduce Global Interpreter Lock. Additionally, we will discuss about the limitations imposed by GIL and how we can potentially find workarounds. We will also discuss about a few relevant concepts such as thread-safety and race conditions.
The Global Interpreter Lock
In CPython, the Global Interpreter Lock (GIL) is a mutex that allows only one thread at a time to have the control of the Python interpreter. In other words, the lock ensures that only one thread is running at any given time. Therefore, it is impossible to take advantage of multiple processors with threads.
GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once – Python Wiki
Since the CPython’s memory management is not thread-safe, the GIL prevents race conditions and ensures thread safety. Threads in Python share the same memory – this means that when multiple threads are running at the same time we don’t really know the precise order in which the threads will be accessing the shared data.
Thread Safety and Race Conditions
A thread-safe code manipulates shared data in a way that it does not interfere with other threads. Therefore, with only one thread running at a time, GIL ensures there will never be race conditions.
In order to better understand what a race condition is, let’s consider a threading example where we have a shared variable called x:
x = 10
Now let’s suppose that two threads are running, performing the operations outlined below:
# Thread 1
x += 10
# Thread 2
x *= 5
Now depending on the order the threads will be accessing the shared variable x we may end up with different results. For example, if we assume that Thread 1 accesses the shared variable x first, the result would be 100.
x += 10 # Thread 1: x = 20
x *= 5 # Thread 2: x = 100
Alternatively, if Thread 2 accesses x first, the result would be different:
x *= 5 # Thread 2: x = 50
x += 10 # Thread 1: x = 60
There’s even a third scenario where both Thread 1 and 2 read the shared variable at exactly the same time. In this case, they will both read in the initial value of x (which is equal to 10) and depending on which thread will write its result last, the resulting value of x will either be 20 (if Thread 1 writes its result last) or 50 (if the second thread writes its result last).
This is an example of what we call a race condition. In other words, a race condition occurs when the behaviour of the system or code relies on the sequence of execution that is defined by uncontrollable events.
And this is exactly what the CPython GIL does. It prevents race conditions by ensuring that only a single thread is running at any given time. This makes life easier for some Python programmers but at the same time it imposes a limitation since multi-core systems cannot be exploited in the context of threading.
Threading vs Multi-processing in Python
As mentioned already, a Python process cannot run threads in parallel but it can run them concurrently through context switching during I/O bound operations. Note that parallelism and concurrency may sound equivalent terms but in practise they are not.
The diagram below illustrates how two threads are being executed and how context switching works when using the [threading](https://docs.python.org/3/library/threading.html) Python library.
Now if you want to take advantage of the computational power of multi-processor and multi-core systems, you may need to take a look at the [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) package that allows processes to be executed in parallel. This is typically useful when it comes to executing CPU-bound tasks.
Multi-processing side-steps the Global Interpreter Lock as it allows for every process spawned to have its own interpreter and thus its own GIL.
For a more comprehensive read around multi-processing and threading in Python, make sure to read the related article shared below that is essentially a deeper dive into concurrency and parallelism with Python.
Sidestepping the GIL in Python
It is important to mention that some alternative Python implementations namely Jython and IronPython have no Global Interpreter Lock and thus they can take advantage of multiprocessor systems.
Now going back to CPython, even though for many programmers the concept of GIL is quite convenient as it makes things much easier. Furthermore, developers don’t really have to interact (or even come across) with the Global Interpreter Lock unless they need to write in C extension. In this situation, you have to release the GIL when the extension does blocking I/O so that other threads within the process can take over and get executed.
The concept of GIL in general though is definitely not ideal given that in certain scenarios modern multiprocessor systems cannot be fully exploited. At the same time though, many long-running or blocking operations are being executed outside the GIL. Such operations include I/O, image processing and NumPy number crunching. Therefore, a GIL becomes a bottleneck only in multithreaded operations that spend time inside the GIL itself.
Getting rid of the Global Interpreter Lock is a common discussion within the Python community. It’s definitely not an easy task to replace GIL as these properties and requirements need to be met.
However, Sam Gross has recently came up with a proof-of-concept implementation of CPython that supports multithreading without the global interpreter lock that is called [nogil](https://github.com/colesbury/nogil/). This PoC essentially demonstrates how to remove GIL in a way that the CPyhton interpreter can scale with added CPU cores.
You can find more details and answers to potential questions in this write-up from the author of nogil. Even if this could be a potential solution, don’t expect that any change will happen any time soon though.
Final Thoughts
In today’s article we discussed about one of the most controversial topics around Python, namely Global Interpreter Lock (aka GIL). We’ve seen what purpose it serves and why it was implemented in the first place but additionally, we discussed the limitations that are imposed as a result of its presence. Furthermore, we discussed about thread-safety and went through an example to demonstrate what a race condition is and how GIL prevents it.
Finally, we discussed about potential ways in which you can eventually sidestep Python GIL and achieve true parallelism by taking advantage of multi-core systems.
Become a member and read every story on Medium. Your membership fee directly supports me and other writers you read. You’ll also get full access to every story on Medium.
You may also like
Share This Article
Towards Data Science is a community publication. Submit your insights to reach our global audience and earn through the TDS Author Payment Program.
Write for TDS