Breaking Down Python Concurrency: The Global Interpreter Lock(GIL) and Its Effect on Multi-threading

RapidFork Technology
5 min readFeb 19, 2023

--

Introduction

Python is a popular high-level programming language that is known for its simplicity, ease of use, and quick development. However, Python’s garbage collection mechanism relies on the Global Interpreter Lock (GIL) which can cause some limitations. This paper will explore the aspects of pointers in Python, particularly focusing on the impact of GIL on memory management, multi-threading, and CPU utilization. Additionally, specific examples will be provided to illustrate the limitations and workarounds.

Memory Management and the Global Interpreter Lock (GIL)

Python uses a garbage collector to automatically manage memory. The garbage collector frees memory by detecting and removing objects that are no longer being used by the program. However, the garbage collector relies on the Global Interpreter Lock (GIL) to function correctly. The GIL is a mechanism that prevents multiple threads from executing Python bytecodes at once. The GIL is necessary because Python’s memory management is not thread-safe, meaning that two threads cannot access the same memory location at the same time without the risk of corrupting data.

The GIL has some implications for memory management. For instance, it can prevent the garbage collector from running simultaneously in multiple threads. As a result, the memory occupied by the objects that are no longer being used will not be cleared until the garbage collector runs. This can lead to memory leaks and degrade performance.

Here’s an example that illustrates this limitation:

import threading

class MyClass:
def __init__(self):
self.my_list = []

def add_value(self, value):
self.my_list.append(value)

my_object = MyClass()

def add_values():
for i in range(1000000):
my_object.add_value(i)

thread_1 = threading.Thread(target=add_values)
thread_2 = threading.Thread(target=add_values)

thread_1.start()
thread_2.start()

thread_1.join()
thread_2.join()

print(len(my_object.my_list))

In this example, we define a class MyClass that has a list my_list. The add_value method adds a value to the list. We create an instance of the class my_object and then create two threads that call the add_values function. This function adds one million values to my_object.my_list. Finally, we print the length of my_object.my_list.

However, due to the GIL, the two threads cannot run in parallel, and the program takes longer to execute. Additionally, the garbage collector may not run during the execution of the threads, which means that the memory used by the values added to my_object.my_list may not be cleared. This can cause a memory leak and degrade performance.

Multi-Threading and the Global Interpreter Lock (GIL)

The Global Interpreter Lock (GIL) also affects multi-threading in Python. The GIL works by putting a lock on each variable and maintaining a usage counter. If a thread wants to access a variable that is already being used by another thread, it must wait until the first thread has released the variable. As a result, only one thread can execute Python bytecodes at once.

This limitation can have implications for multi-threaded programs that rely heavily on CPU-bound operations. For example, if a program has two threads that perform complex calculations, the GIL will prevent them from running in parallel, and the program will not benefit from the use of multiple CPU cores.

Here’s an example that illustrates this limitation:

import threading

def fib(n):
if n <= 1:
return n
else:
return fib(n-1) + fib(n-2)

def compute_fib():
for i in range(30):
print(fib(i))

thread_1 = threading.Thread(target=compute_fib)
thread_2 = threading.Thread(target=compute_fib)

thread_1.start()
thread_2.start()

thread_1.join()
thread_2.join()

In this example, we define a `fib` function that computes the nth Fibonacci number. We also define a `compute_fib` function that computes the first 30 Fibonacci numbers by calling the `fib` function. We create two threads that call the `compute_fib` function and then start them. Finally, we wait for the threads to finish using the `join` method. However, due to the GIL, the two threads cannot execute Python bytecodes in parallel. As a result, the program does not benefit from the use of multiple CPU cores, and the execution time is the same as if we had used a single thread. CPU Utilization and Multi-Processing To utilize multiple CPU cores in Python, we can use the multiprocessing module. The multiprocessing module allows us to create multiple processes that run in parallel, each with its own interpreter and memory space. This means that each process can use its own CPU core to execute Python bytecodes without being affected by the GIL.

Here’s an example that illustrates how we can use the multiprocessing module to compute the Fibonacci sequence:

import multiprocessing

def fib(n):
if n <= 1:
return n
else:
return fib(n-1) + fib(n-2)

def compute_fib(start, end):
for i in range(start, end):
print(fib(i))

if name == 'main':
with multiprocessing.Pool(processes=2) as pool:
pool.starmap(compute_fib, [(0, 15), (15, 30)])

In this example, we define a `fib` function that computes the nth Fibonacci number. We also define a `compute_fib` function that computes the Fibonacci sequence for a range of numbers by calling the `fib` function. We create a pool of two processes using the `multiprocessing.Pool` method and then use the `starmap` method to execute the `compute_fib` function for two ranges of numbers (0 to 15 and 15 to 30). The `starmap` method distributes the ranges of numbers across the two processes, and each process computes the Fibonacci sequence for its assigned range. Finally, the results are printed.

By using the multiprocessing module, we can utilize multiple CPU cores to execute Python bytecodes in parallel without being affected by the GIL. This can significantly improve the performance of CPU-bound operations.

Conclusion

In conclusion, pointers in Python can be affected by the Global Interpreter Lock (GIL) when it comes to memory management, multi-threading, and CPU utilization. The GIL is necessary to prevent multiple threads from accessing the same memory location at the same time, but it can limit the performance of multi-threaded and CPU-bound programs. To work around the GIL, we can use the multiprocessing module to utilize multiple CPU cores and execute Python bytecodes in parallel, each with its own interpreter and memory space. By doing so, we can improve the performance of CPU-bound operations and overcome the limitations of the GIL.

Bonus: Performance Comparison — Multiprocessing vs Multi-threading(minus GIL constraint)

In a truly multi-threaded environment where the GIL is not a constraint, we can achieve better performance by using multi-threading instead of multiprocessing. This is because multi-threading has less overhead than multiprocessing as it does not require the creation of new processes and does not need to serialize/deserialize data between processes.

Here’s an example that illustrates how we can use multi-threading to compute the Fibonacci sequence in a truly multi-threaded environment:

import threading

def fib(n):
if n <= 1:
return n
else:
return fib(n-1) + fib(n-2)

def compute_fib(start, end):
for i in range(start, end):
print(fib(i))

if __name__ == '__main__':
thread_1 = threading.Thread(target=compute_fib, args=(0, 15))
thread_2 = threading.Thread(target=compute_fib, args=(15, 30))

thread_1.start()
thread_2.start()

thread_1.join()
thread_2.join()

In this example, we define a fib function that computes the nth Fibonacci number. We also define a compute_fib function that computes the Fibonacci sequence for a range of numbers by calling the fib function. We create two threads that call the compute_fib function and then start them. Finally, we wait for the threads to finish using the join method.

In a truly multi-threaded environment, this approach can achieve better performance than the multiprocessing approach. However, in a GIL-constrained environment like Python, this approach does not perform as well because the GIL limits the execution of Python bytecodes in parallel across threads.

To conclude, when the GIL is not a constraint, we can achieve better performance by using multi-threading instead of multiprocessing. However, in a GIL-constrained environment like Python, multi-threading does not perform as well as multiprocessing. In such cases, we can use multiprocessing to utilize multiple CPU cores and execute Python bytecodes in parallel without being affected by the GIL.

--

--