A global interpreter lock (GIL) is a mechanism to apply a global lock on an interpreter. It is used in computer-language interpreters to synchronize & manage the execution of threads so that only one native thread (scheduled by the operating system) can execute at a time.
In a scenario where we have multiple threads, what can happen is that both the thread might try to acquire the memory at the same time, & as a result of which they would overwrite the data in the memory. Hence, arises a need to have a mechanism that could help prevent this phenomenon.
Some popular interpreters that have GIL are CPython & Ruby MRI. As most of us would know that Python is an interpreted language, it has various distributions like CPython, Jython, IronPython. Out of these, GIL is supported only in CPython, & it is also the most widely used implementation of Python. CPython has been developed in both C & Python language primarily to support & work with applications that have a lot of C language underneath the hood.
Even if our processor has multiple cores, a global interpreter will allow only one thread to be executed at a time. This is because, when a thread starts running, it acquires the global interpreter lock. When it waits for any I/O operation ( reading/writing data from/to disk ) or a CPU bound operation ( vector/matrix multiplication ), it releases the lock so that other threads of that process can run. Hence, it prevents us from running the other threads at the same time.
Let’s take a moment & understand the above diagram. As we can see, there is a
factorialfunction & two threads 1 & 2, thread 1 is in the locked state while thread 2 is in a wait state. This means that only one of the threads is able to access the function. Now, let's assume that the
factorial function takes 2 seconds to complete. Then, in an ideal case, both the threads should be able to finish the execution in 2 seconds. However, this is not the case in Python, both the threads will run serially & not parallel to each other.
The threads 1 & 2 calling the
factorial function may take twice as much time as a single thread calling the function twice. This also tells us that the memory manager governed by the interpreter is not
thread-safe, which means that multiple threads fail to access the same shared data simultaneously.
- Limits the threading operation
- Parallel execution is restricted
- The GIL ensures that only one thread runs in the interpreter at once
- It helps in simplifying various low-level details like memory management
With GIL cooperative computing or coordinated multitasking is achieved instead of parallel computing.
As it can be observed from the above diagram, there are three threads initially: thread 1 is running & it has acquired the GIL, & when an I/O operation like a read, write, etc. is done, thread 1 releases the GIL & it is then acquired by thread 2. This cycle keeps on going, & GIL keeps changing threads alternatively till the threads have completed the execution of the program. Remember that if any of the threads that do not have the lock & have not completed the execution, they will then be in a waiting state.
Global Interpreter Lock in Python 2.7 works differently when compared to Python3. In a pure CPU-bound operation, the thread continues to run since there is no I/O operation in which case the other threads will be in an idle or a wait state, which is not what you would want. To solve this issue, Python2 has terminology known as
ticks. The global interpreter lock performs a check to monitor the state of thread-like wait state, I/O operation, or whether it is being run. However, GIL does not keep monitoring the threads at every instance or time, rather it uses the concept of
ticks, & it checks the threads at every 100 ticks.
A tick is a byte-code instruction, & when the 100 byte-code instructions are completed, the GIL checks whether the threads are running or in a waiting state. It is important to remember that a tick is not related to time since it is a byte-code instruction. Each tick might take a longer or shorter time to run compared to the other.
This periodic check of monitory the threads after every 100 ticks is essential, especially in CPU bound operations since they do not have any I/O operations. Note that we can modify the tick counter using the built-in
sys module of Python.
Let’s discuss some of the demerits of the above approach:
Now, if we look at this diagram, we may notice that the threads are not running parallel to each other but in a serial order, also known as cooperative computing governed by the global interpreter lock. Also, the threads have to wait for longer durations. For example, the thread T3 had to wait for both the threads T1 & T2 to release the GIL. Hence, the threads starve for the lock & the compute.
Another demerit is that there is a battle between the threads as to which thread will acquire the GIL. Initially, it might be the case that based on the priority T1 would have got the GIL, then T2, & so on. However, when T3 releases GIL, it will send a notification to all other threads, in which case all other threads will keep fighting to acquire the GIL. Having said that, we could potentially overcome this by setting the priorities of each thread.
In Python3, we have fixed-time allocated for each thread, i.e., 5ms of execution time. This prevents the threads from starving for resources since the waiting time is equal for all the threads. The threads will wait to see if the GIL gets released by Thread 1 on purpose because of I/O or sleep operation. If not, it will force it to release after 5ms. Hence, there will not be any starvation of CPU time since every thread will get an equal amount of time, & this will eliminate the challenge of acquiring the GIL.
However, it is important to note that the new implementation of GIL gives more priority to CPU-bound jobs as compared to the I/O operations. In a case where a thread has released the GIL, & two threads are waiting, GIL will be acquired by the thread which has a requirement of running a CPU-bound job as compared to the other thread.
This tutorial covered an advanced topic, & the purpose was to give us all a theoretical overview of how exactly GIL works in Python. Since this tutorial did not cover any coding aspects of GIL & threads, one good exercise for us all would be to experiment on our own by creating multiple threads & a method that performs an I/O intensive task & finally analyzes how the GIL is switched between each thread.