What is Python GIL?
What is an Interpreter?
As an interpreted language, Python first compiles source code into bytecode and then executes it in a virtual machine, with the entire process handled by the interpreter. Similar languages include JavaScript and PHP.
However, there are multiple interpreters. The official one is CPython, developed in C. There are also others like Jython (Java-based) and RPython (R-based).

What is GIL?
GIL is a mutual exclusion lock.
What is a Mutual Exclusion Lock?
In programming, the concept of object-level mutexes is introduced to ensure the integrity of shared data. Each object corresponds to a "mutex" marker, which ensures that at any given time, only one thread can access the object.
When multiple threads almost simultaneously modify a shared data item, synchronization control is needed.
Thread synchronization guarantees safe access to competing resources. The simplest synchronization mechanism is introducing a mutex.
A mutual exclusion lock introduces a state to resources: locked/unlocked.
When a thread modifies a shared resource, it first locks it, making the resource's state "locked," and other threads cannot modify it until the thread releases the resource, changing the resource's state to "unlocked." A mutual exclusion lock ensures that only one thread performs write operations at a time, guaranteeing data correctness in multi-threaded scenarios.
Why is GIL Introduced?
GIL is introduced because CPython's memory management is not safe.
In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. The GIL prevents race conditions and ensures thread safety. A nice explanation of how the Python GIL helps in these areas can be found here. In short, this mutex is necessary mainly because CPython’s memory management is not thread-safe.
In CPython, the GIL is a mutual exclusion lock that allows only one thread to execute bytecode at any given time. This avoids race conditions and ensures thread safety. Simply put, the existence of a mutual exclusion lock is due to CPython's memory management being non-thread-safe.
According to Python's official documentation, since CPython's memory management is not "thread-safe," a mutex is needed, leading to two additional questions.
GIL is introduced because CPython's memory management is unsafe
Why is CPython's Memory Management Unsafe?
Python was first released in 1991. At that time, CPUs were single-core, and multi-threading was mainly designed for doing I/O while performing CPU calculations. The Python compiler was written in C, hence called CPython. Many programming languages lacked automatic memory management at that time. To achieve automatic garbage collection, Python implemented reference counting for each object. When the reference count reaches zero, the object can be recycled and memory released, for example:
>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3
Here, the 'a' object has 3 references,
- One is itself,
- One is variable 'b',
- One is the parameter of getrefcount function.
If another thread references 'a', the reference count increases by 1. If a thread uses 'a' and then ends, the reference count decreases by 1. Multiple threads modifying the same variable's "reference count" can lead to race conditions.
How to Solve the Memory Management Insecurity Issue?
To avoid race conditions, the simplest and most effective method is to add a mutual exclusion lock. However, if a mutex is added to every object, it may cause another problem: deadlocks, and frequent acquisition and release can reduce performance.
Therefore, the simplest and most effective solution is to add an interpreter lock. Threads must acquire the interpreter lock before executing any bytecode, which avoids deadlocks and has minimal performance impact. At that time, CPUs were single-core, and this GIL design was simple and did not affect performance, so it has been used until today. The main reason for the existence of GIL is because Python's memory management is not thread-safe, which is the primary cause of GIL's creation and persistence.

Mutual Exclusion Lock Code Example
The threading module defines the Lock class, which can easily handle locking:
# Create a lock
mutex = threading.Lock()
# Acquire the lock
mutex.acquire()
# Release the lock
mutex.release()
- If this lock was not previously locked, acquire will not block.
- If another thread has already locked this lock before calling acquire, it will block until the lock is released.
Mutual Exclusion Lock Outside of for Loop
import threading
import time
# Define a global variable
g_num = 0
def test1(num):
global g_num
# Acquire the lock; if it was not previously locked, this will succeed
# If the lock was already locked by another thread before calling acquire, it will block until released
mutex.acquire()
for i in range(num):
g_num += 1
mutex.release() # Release the lock
print("-----in test1 g_num=%d----" % g_num)
def test2(num):
global g_num
mutex.acquire() # Acquire the lock
for i in range(num):
g_num += 1
mutex.release() # Release the lock
print("-----in test2 g_num=%d=----" % g_num)
# Create a mutual exclusion lock; it is initially unlocked
mutex = threading.Lock()
def main():
t1 = threading.Thread(target=test1, args=(1000000,))
t2 = threading.Thread(target=test2, args=(1000000,))
t1.start()
t2.start()
# Wait for the two threads to complete
time.sleep(2)
print("-----in main Thread g_num = %d---" % g_num)
if __name__ == "__main__":
main()
#-----in test1 g_num=1000000----
#-----in test2 g_num=2000000=----
#-----in main Thread g_num = 2000000---
Mutual Exclusion Lock Inside for Loop
import threading
import time
# Define a global variable
g_num = 0
def test1(num):
global g_num
for i in range(num):
mutex.acquire() # Acquire the lock
g_num += 1
mutex.release() # Release the lock
print("---test1---g_num=%d"%g_num)
def test2(num):
global g_num
for i in range(num):
mutex.acquire() # Acquire the lock
g_num += 1
mutex.release() # Release the lock
print("---test2---g_num=%d"%g_num)
# Create a mutual exclusion lock
# It is initially in an unlocked state
mutex = threading.Lock()
# Create two threads to increment g_num by 1,000,000 times each
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()
p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()
# Wait for the calculations to complete
while len(threading.enumerate()) != 1:
time.sleep(1)
print("The final result after two threads operate on the same global variable is:%s" % g_num)
# ---test1---g_num=1909909
# ---test2---g_num=2000000
# The final result after two threads operate on the same global variable is:2000000
Locking and Unlocking Process
- When a thread calls the acquire() method of the lock to obtain it, the lock enters the "locked" state.
- Only one thread can obtain the lock at a time. If another thread attempts to obtain this lock while it is locked, that thread will become "blocked," known as "blocking," until the thread holding the lock calls release() to unlock it.
- The thread scheduler selects a thread from those in a synchronized blocking state to acquire the lock and transitions it into the running (running) state.
Benefits of Locking
- Ensures that a critical section of code can be executed by only one thread from start to finish.
Drawbacks of Locking
- Prevents concurrent execution of multiple threads, with the code containing locks actually executing in single-threaded mode, significantly reducing efficiency.
- Due to the possibility of having multiple locks, different threads may hold different locks and attempt to acquire each other's locks, potentially causing deadlocks.
Deadlock Code Example
import threading
import time
# Create a mutual exclusion lock
lock = threading.Lock()
# According to the index to get values, ensuring that only one thread can access it at the same time
def get_value(index):
# Acquire the lock
lock.acquire()
print(threading.current_thread())
my_list = [3,6,8,1]
# Check for index overflow
if index >= len(my_list):
print("Index out of bounds:", index)
return
value = my_list[index]
print(f'Value is:{value}')
time.sleep(0.2)
# Release the lock
lock.release()
if __name__ == '__main__':
# Simulate a large number of threads executing get_value operations
for i in range(30):
sub_thread = threading.Thread(target=get_value, args=(i,))
sub_thread.start()
In cases of deadlock, the program cannot stop normally and keeps waiting.
<Thread(Thread-1, started 30364)>
Value is:3
<Thread(Thread-2, started 27120)>
Value is:6
<Thread(Thread-3, started 29632)>
Value is:8
<Thread(Thread-4, started 29988)>
Value is:1
<Thread(Thread-5, started 20984)>
Index out of bounds: 4
Avoiding Deadlock Code Example
# Release the lock at appropriate places
import threading
import time
# Create a mutual exclusion lock
lock = threading.Lock()
# According to the index to get values, ensuring that only one thread can access it at the same time
def get_value(index):
# Acquire the lock
lock.acquire()
print(threading.current_thread())
my_list = [3,6,8,1]
if index >= len(my_list):
print("Index out of bounds:", index)
# When the index is out of bounds, release the lock to allow subsequent threads to access it
lock.release()
return
value = my_list[index]
print(value)
time.sleep(0.2)
# Release the lock
lock.release()
if __name__ == '__main__':
# Simulate a large number of threads executing get_value operations
for i in range(10):
sub_thread = threading.Thread(target=get_value, args=(i,))
sub_thread.start()
<Thread(Thread-1, started 30336)>
3
<Thread(Thread-2, started 5920)>
6
<Thread(Thread-3, started 28308)>
8
<Thread(Thread-4, started 27324)>
1
<Thread(Thread-5, started 26840)>
Index out of bounds: 4
<Thread(Thread-6, started 30104)>
Index out of bounds: 5
<Thread(Thread-7, started 28900)>
Index out of bounds: 6
<Thread(Thread-8, started 2676)>
Index out of bounds: 7
<Thread(Thread-9, started 28912)>
Index out of bounds: 8
<Thread(Thread-10, started 30068)>
Index out of bounds: 9
Process finished with exit code 0
Finally, What Results Does GIL Cause?
Positive
Solving safety issues.
Negative
Single-threaded CPU Consumption
About 16% (i5 11th generation)
import threading
def dead_loop():
while True:
pass
dead_loop()
Dual-threaded CPU Consumption
Still about 16%, not 32%.
import threading
def dead_loop():
while True:
pass
# Start a new dead loop thread
t = threading.Thread(target=dead_loop)
t.start()
# Main thread also enters a dead loop
dead_loop()
t.join()
dead_loop()
Conclusion:
- When using two threads, Cpython uses the same amount of CPU resources as a single-threaded scenario.
- CPython can only run one GIL thread at a time.

If you further try ten or N threads, Python's CPU utilization remains unchanged.
However, rewriting the same dead loop in C, C++, or Java can fully utilize all cores. Why is Python not able to do this? It is precisely the GIL.
Python threads are truly threads, but when the interpreter executes code, it has a GIL lock: Global Interpreter Lock. Any Python thread must first acquire the GIL lock before executing, and after every 100 bytecode instructions, the interpreter automatically releases the GIL lock, allowing other threads to execute. This GIL global lock actually locks all threads' execution codes, so multi-threading in Python can only alternate execution, even if 100 threads are running on a 100-core CPU, they can only use one core.
In Python, you can use multi-threading, but you shouldn't expect to effectively utilize multi-core processors. If you really want to use multi-threading to utilize multi-core processors, you have to implement it through C extensions, although this loses the simplicity and ease of use that Python is known for.
However, there's no need to worry too much. Although Python cannot use multi-threading to achieve multi-core tasks, it can achieve multi-core tasks through multi-processes. Multiple Python processes have their own independent GIL locks and do not affect each other.
Other Notes
- The expression "Python's GIL" is not rigorous but is not wrong.
- The GIL is relative to the Cpython interpreter, not the Python language itself.
- Cpython is used to parse Python code.
- Cpython is currently the most popular and mainstream interpreter.
Finally, as long as you are willing, you can develop your own interpreter without a GIL.