Pandora ResearchPandora
Research
RUEN
Python

The GIL: Python's Global Interpreter Lock

Pandora ResearchPandora Research
July 9, 20265 min read

When working with threads, and also when discussing performance in Python, you often hear about the GIL (Global Interpreter Lock) problem. In short, the GIL allows only one thread to run inside a process at a time. This reduces the performance of multithreaded programs.

When several threads run in Python, conflicts are possible when accessing the same regions of memory, so a global lock is needed here to manage them correctly. While running, Python counts the number of references to objects so that the garbage collector works correctly. Everything in Python is an object, and each object has a refcount attribute that stores the number of references to it. As soon as it drops to zero, the object is removed from memory.

Reference counting

Let's look at a small example of counting references to an object.

Python
import sys

s = "подсчет количества ссылок на объект"
print(sys.getrefcount(s))
s2 = s
print(sys.getrefcount(s))

As a result, the console prints:

Terminal
2
3

When the getrefcount function is called and the variable s is passed into it, the function argument also references the object. That is why there are initially two references right after the function call is created. Then, after assigning the variable s2, the number of references becomes three.

What problem the GIL solves

One of the problems the GIL solves is the following: several threads in a multithreaded application can modify a variable's reference counter at the same time. This can lead to the reference count reaching zero at some point and the object being removed from memory, even though it should still have been used later on. As a result, the code would work incorrectly.

The reference counter can be protected by adding locks to all data structures shared across multiple threads. In that case the counter would be modified sequentially. But adding a lock to several objects can lead to another problem — a deadlock. It happens when a lock is placed on more than one object. On top of that, this would also reduce performance because of acquiring the lock repeatedly.

Ultimately, the GIL turns any multithreaded program into a single-threaded one.

When threads are still useful

But is it worth working with threads in Python at all, given all these limitations? The answer is yes. Threads can be used if the parallelized code is mostly busy with input/output operations (IO-bound operations): database queries, reading or writing files, requests to external URLs. In that case the variables — and therefore the reference counter — will not change, and the GIL simply hands control to another thread while the first one waits for the I/O operation to finish.

The simplest way to work around the GIL is to use multiple processes instead of threads. But in that case you need to keep in mind that processes do not share memory, and you have to take extra care to pass data between them.

Python

Got a task for our team?

Tell us about the project — we'll assess it and propose a solution within one business day.

Discuss a project