The GIL is a simple boolean flag in the CPython source code (Python/ceval_gil.h), protected by an OS-level mutex.
Before the CPython evaluation loop (_PyEval_EvalFrameDefault) is allowed to execute the next bytecode instruction, it must acquire this lock.
Why does it exist? CPython uses reference counting (ob_refcnt) for memory management. If two threads increment the reference count of the exact same object simultaneously, the updates might overwrite each other, causing the count to drop to 0 prematurely, resulting in a fatal segfault. The GIL completely prevents this by ensuring only one thread can ever manipulate Python objects.
Because of the GIL, if Thread A is doing a large mathematical calculation, it could potentially hoard the GIL forever, starving all other threads.
To prevent this, CPython has an internal ticker called the switch interval (default: 5 milliseconds).
Every 5 milliseconds, the active thread is forced to drop the GIL and request it again. This gives other waiting threads a chance to grab the GIL and execute a slice of their bytecode. You can view or change this interval using sys.getswitchinterval() and sys.setswitchinterval().
The GIL is a Python-level construct. It only restricts the execution of Python bytecode. If you write a C extension, you can explicitly release the GIL before doing heavy computation.
Py_BEGIN_ALLOW_THREADS
// ... heavy C-level array math (e.g. NumPy) ...
Py_END_ALLOW_THREADSThis is the secret behind why libraries like NumPy and Pandas are so incredibly fast in Python. When you call a large matrix multiplication in NumPy, it drops the GIL, shifts the workload down into C, and spawns native C threads to execute across all your CPU cores in true parallelism. It then re-acquires the GIL just to return the result back to Python.