Understanding Python GIL
Python developers often encounter the term "GIL," which is often considered a major drawback of the Python programming language. This article will explain what the GIL is, how effective it is at preventing common concurrency issues
The Global Interpreter Lock (GIL) in Python is a mechanism that permits only one thread to execute Python byte-code at a time, even on multi-core systems.
To illustrate this, let's examine the following code.
This code showcases a simple threading example for a heavy computational task
Ran in 8.15 seconds
It ran in some 8.15 seconds.
Now let's have a peek into another code which features multiple thread into same existing code.
This is the same code as above, but instead of using only one thread we are using two thread to execute the counter function in parallel.
Let's analyze output of following code.
Ran in 13.84 seconds
This may surprise those who aren't familiar with GIL. Despite running two threads in parallel, the program's execution time actually increases, which doesn't seem to make sense. If one thread takes 8 seconds, shouldn't both threads executing in parallel take the same amount of time?
Well turns out no. Remember GIL is a mechanism that permits only one thread to execute at time. Same behavior is reflected in above code although two thread were executed in parallel only one them was computing at a given time which almost doubles the execution time for multi-threaded code.
Now this arises the question why is GIL actually needed if it prevents multi-thread execution. There are actually alternatives implementation of python
The GIL is necessary because CPython, the reference implementation of Python, is not thread-safe at the bytecode level.
Notice the keyword byte-code, let's understand that first. As many of us understand that GIL limits python to execute one thread at a time limiting the CPU-bound tasks. If the execution is limited to one thread at a time, why do synchronization mechanism such as locks exists in python, doesn't one thread execution at a time mitigates concurrency problem of race condition itself.
Let's look into some examples
The output of the above code yields in following result, (using python 3.11)
Iteration 0: x = 20000000
Iteration 1: x = 20000000
Iteration 2: x = 20000000
Iteration 3: x = 20000000
Iteration 4: x = 20000000
Iteration 5: x = 20000000
Iteration 6: x = 20000000
Iteration 7: x = 20000000
Iteration 8: x = 20000000
Iteration 9: x = 20000000
So on every iteration, we get the same output 20000000, must be because at one time only one thread executes due to GIL is what seems like obvious answer but cannot be further from truth. To understand better let's use another version of python and see the result. (Using python 3.9)
Iteration 0: x = 16584223
Iteration 1: x = 16841017
Iteration 2: x = 16730662
Iteration 3: x = 16774960
Iteration 4: x = 17255257
Iteration 5: x = 16761488
Iteration 6: x = 16971182
Iteration 7: x = 16679143
Iteration 8: x = 16746955
Iteration 9: x = 16612515
Now that yields to completely different sets of result, we encountered race condition on all of the 10 iterations since none of them resulted in value of 2000000 which was expected output.
领英推荐
The understanding of word byte-code, now makes much more sense. GIL ensures that a single thread is running for time interval of 5 milliseconds, after that if a different thread requests the GIL, the currently running thread is interrupted and forced to give up the lock, allowing the other thread to acquire it. GIL doesn't prevent race condition because it depends on code as to whether or not the logic/function completes its execution within the time it has acquired the lock, if not another thread will simultaneously occupy the lock leaving the previous execution in midst thus resulting in race condition.
The reason we are getting different output using different version of python lies in how python 3.11 ensures locking mechanism by default and how python 3.9 does not. Interestingly enough race conditions are prevalent even with presence of GIL however using different version might yield in confusing results.
To understand in simpler terms python3.11 computes the above code that can be represented in 3.9 manner in following snippet
Notice how before calling the increment() function during each iteration, threads are required to acquire a lock. Locking ensures that only one thread at given instance of time has access to the variable, and this is exactly how python 3.11 ensures that same result is acquired every time. Executing the following code using python3.9 yields on similar output we got from python3.11
Iteration 0: x = 20000000
Iteration 1: x = 20000000
Iteration 2: x = 20000000
Iteration 3: x = 20000000
Iteration 4: x = 20000000
Iteration 5: x = 20000000
Iteration 6: x = 20000000
Iteration 7: x = 20000000
Iteration 8: x = 20000000
Iteration 9: x = 20000000
To ensure that python 3.11 is indeed using lock internally, and GIL has no role whatsoever in the example presented above, copy the above script (one without lock implementations) execute it and on midst of execution exit it (using ctrl+c), and you'll see something along the lines of,
File "/home/saksham/Projects/gil-inspection/main.py", line 24, in main_task
t1.join()
^^^^^^^^^
File "/usr/lib/python3.11/threading.py", line 1119, in join
self._wait_for_tstate_lock()
File "/usr/lib/python3.11/threading.py", line 1139, in _wait_for_tstate_lock
if lock.acquire(block, timeout):
on console. If you take a closer look you'll realize python3.11 ensure thread locking mechanism by default which might result in confusing outcome and misunderstanding about the GIL for those who are beginning their journey in python.
Summary :
The code gists used in above article can be found at,