Change language

What is Python Global Interpreter Lock and how GIL works

|

The Python Global Interpreter Lock (GIL) is a kind of lock which allows only one thread to control the Python interpreter. This means that only one particular thread will be running at any given time.

The operation of GIL may seem unimportant to developers creating single-threaded programs. But in multi-threaded programs, the absence of GIL can have a negative impact on the performance of CPU-hungry programs.

Because GIL only allows a single thread to run, even in a multithreaded application, it has earned the reputation of being an "infamous" feature.

This article will discuss how GIL affects application performance, and how this very impact can be mitigated.

Featured review: Best laptop for Machine Learning

What problem does GIL solve in Python?

Python counts the number of references for correct memory management. This means that objects created in Python have a reference counting variable that stores the number of all references to that object. Once this variable reaches zero, the memory allocated to that object is freed.

Here's a small code sample to demonstrate how reference counting variables work:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

In this example the number of references to the empty array is 3. This array is referenced by: variable a, variable b and the argument passed to sys.getrefcount().

The problem GIL solves is that in a multithreaded application, multiple threads can increment or decrement this reference count. This can cause memory to be cleared incorrectly, and an object that is still referenced will be deleted.

You can protect the reference counter by adding locks to all the data structures that are distributed across multiple threads. In this case, the counter will be changed only consistently.

But adding locks to multiple objects can introduce another problem, deadlocks, which only occur if there is a lock on more than one object. In addition, this problem would also degrade performance by installing locks multiple times.

GIL is a single lock of the Python interpreter itself. It adds a rule: any execution of bytecode in Python requires locking the interpreter. Interlocking can then be eliminated, since GIL will be the only lock in the application. In addition, its effect on CPU performance is not critical at all. But you should keep in mind that GIL surely makes any program single-threaded.

Although GIL is used in other interpreters, such as Ruby, it is not the only solution to this problem. Some languages solve the thread-safe memory release problem with rubbish collection.

On the other hand, this means that such languages must often compensate for the loss of single-threaded GIL advantages by adding some additional performance enhancing features, such as JIT compilers.

Why was GIL chosen as the solution?

So, why is this not a very "good" solution used in Python? How critical is this solution for developers?

According to Larry Hastings, the GIL architectural solution is one of the things that made Python popular.

Python has been around since the days when there was no concept of threads in operating systems. The language was designed to be easy to use and to speed up development. More and more developers switched to Python.

Many of the extensions that Python needed were written for existing C libraries. To prevent inconsistent changes, the C language required thread-safe memory management, which GIL was able to provide.

GIL could be easily implemented and integrated into Python. It increased the performance of single-threaded applications because only one locker was managed.

Those C libraries that were not thread-safe became easier to integrate. These C extensions were one of the reasons why the Python community began to expand.

As you can see, GIL is a de facto solution to the problem CPython developers faced early in Python's life.

See also: Best laptop for hacking

The impact of GIL on multithreaded applications

If you look at a typical program (not necessarily written in Python) - there is a difference whether that program is limited to CPU performance or I/O.

CPU-bound operations are all computational operations: matrix multiplication, search, image processing, etc.

I/O-bound operations are those that are often waiting for something from I/O sources (user, file, database, network). Such programs and operations can sometimes wait a long time until they receive what they need from the source. This is because the source may perform its own (internal) operations before it is ready to output the result. For example, a user may think about what to type in a search box or what query to send to the database.

Below is a simple CPU-bound program that simply counts down:

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n > 0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time spent -', end - start)

Running this on a 4-core computer produces this result:

Time spent - 6.20024037361145

Below is the same program, with a slight modification. Now the countdown is done in two parallel threads:

# multi_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n > 0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time spent -', end - start)

And here is the result:

$ python multi_threaded.py
Time spent - 6.924342632293701

As you can see from the results, both options took about the same amount of time. In the multithreaded version, GIL prevented parallel execution of threads.

GIL does not greatly affect the performance of I/O operations in multithreaded programs because the lock is propagated across threads while waiting for I/O.

But a program whose threads will work exclusively with a processor (for example, processing an image in parts) will not only become a single-threaded program because of a lock, but its execution will take more time than if it had been a strictly single-threaded program originally.

This increase in time is a result of the appearance and implementation of locking.

Why is GIL still in use?

Language developers have received plenty of complaints about GIL. But a language as popular as Python cannot make such a drastic change as to remove GIL, because that would naturally lead to a lot of incompatibility problems.

There have been attempts in the past by developers to remove GIL. But all those attempts were destroyed by existing C extensions which relied heavily on existing GIL solutions. Naturally, there are other options that are similar to GIL. However, they either degrade the performance of single-threaded and multi-threaded I/O applications, or are simply difficult to implement. You wouldn't want your program to run slower in new versions than it does now, would you?

Python's creator, Guido van Rossum, commented on this in a September 2007 article titled "It isn't Easy to remove the GIL":

"I would only be happy with patches in Py3k if the performance of single-threaded applications or multi-threaded I/O applications was not reduced."

Since then, none of the attempts made have satisfied this condition.

Why wasn't GIL removed in Python 3?

Python 3 actually had the ability to redesign some functions from scratch, although this would have caused many C extensions to simply break and have to be redesigned. This is why the first versions of Python 3 had so little traction in the community.

But why not remove GIL in parallel with the Python 3 upgrade?

Removing it would make single-threading in Python 3 slower than in Python 2, and just imagine what that would entail. You can't help but notice the benefits of single-threading in GIL. That's why it's still not removed.

But Python 3 does provide improvements to the existing GIL. Up to this point, the article has talked about the effect of GIL on multithreaded programs that only affect the CPU or only I/O. But what about those programs that have some threads going to the CPU and some going to I/O?

In such programs, I/O threads "suffer" because they don't have access to GIL from processor threads. This is due to a mechanism built into Python that forces threads to release GIL after a certain interval of continuous use. In case no one else was using GIL, these threads could continue running.

But there is one problem here. Almost always GIL is occupied by processor threads and the other threads do not have time to take over. This fact was studied by David Beazley, a visualisation of it can be seen here.

The problem was fixed in Python 3.2 in 2009 by developer Antoine Pitrou. He added a mechanism to count threads which need GIL. And if there are other threads that need GIL, the current thread wouldn't take their place.

How do you handle GIL?

If GIL is causing you problems, here are some solutions you can try:

Multiprocessing vs. multi-threading. A fairly popular solution because each Python process has its own interpreter with memory allocated to it, so GIL won't be a problem. Python already has a multiprocessing module that makes it easy to create processes like this:

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time spent in seconds -', end - start)

After launching, we get this result:

Time spent in seconds - 4.060242414474487

You can notice a decent increase in performance compared to the multi-threaded version. However, the time indicator is not reduced by half. All due to the fact that process management itself affects performance. Multiple processes are more complex than multiple threads, so you need to handle them carefully.

Alternative Python interpreters. Python has many different implementations of interpreters. CPython, Jyton, IronPython and PyPy written in C, Java, C# and Python respectively. GIL exists only on the original interpreter - CPython.

You can just take advantage of single-threadedness while some of the brightest minds right now are working to eliminate GIL from CPython. Here's one of the attempts.

Often, GIL is seen as something complicated and obscure. But keep in mind that as a python developer, you will only encounter GIL if you are writing C extensions or multithreaded CPU programs.

At this point you should understand all the aspects required when working with GIL

Shop

Learn programming in R: courses

$

Best Python online courses for 2022

$

Best laptop for Fortnite

$

Best laptop for Excel

$

Best laptop for Solidworks

$

Best laptop for Roblox

$

Best computer for crypto mining

$

Best laptop for Sims 4

$

Latest questions

NUMPYNUMPY

Common xlabel/ylabel for matplotlib subplots

12 answers

NUMPYNUMPY

How to specify multiple return types using type-hints

12 answers

NUMPYNUMPY

Why do I get "Pickle - EOFError: Ran out of input" reading an empty file?

12 answers

NUMPYNUMPY

Flake8: Ignore specific warning for entire file

12 answers

NUMPYNUMPY

glob exclude pattern

12 answers

NUMPYNUMPY

How to avoid HTTP error 429 (Too Many Requests) python

12 answers

NUMPYNUMPY

Python CSV error: line contains NULL byte

12 answers

NUMPYNUMPY

csv.Error: iterator should return strings, not bytes

12 answers

News


Wiki

Python | How to copy data from one Excel sheet to another

Common xlabel/ylabel for matplotlib subplots

Check if one list is a subset of another in Python

sin

How to specify multiple return types using type-hints

exp

Printing words vertically in Python

exp

Python Extract words from a given string

Cyclic redundancy check in Python

Finding mean, median, mode in Python without libraries

cos

Python add suffix / add prefix to strings in a list

Why do I get "Pickle - EOFError: Ran out of input" reading an empty file?

Python - Move item to the end of the list

Python - Print list vertically