Curso
Si estás empezando con Python y quieres seguir aprendiendo, haz el curso Introduction to Data Science in Python de DataCamp.
Un global interpreter lock (GIL) es un mecanismo que aplica un bloqueo global al intérprete. Se utiliza en intérpretes de lenguajes para sincronizar y gestionar la ejecución de hilos de manera que solo un hilo nativo (planificado por el sistema operativo) pueda ejecutarse a la vez.
En un escenario con varios hilos, puede ocurrir que dos hilos intenten acceder a la misma zona de memoria al mismo tiempo y acaben sobrescribiendo los datos. Por eso es necesario un mecanismo que evite este problema.
Algunos intérpretes conocidos que usan GIL son CPython y Ruby MRI. Como sabrás, Python es un lenguaje interpretado con distintas implementaciones: CPython, Jython, IronPython. De todas ellas, solo CPython incorpora GIL, y además es la implementación más utilizada. CPython está desarrollado en C y Python principalmente para integrarse bien con aplicaciones que tienen mucho código en C bajo el capó.
Aunque tu procesador tenga varios núcleos, el bloqueo global del intérprete permite que solo se ejecute un hilo cada vez. Esto se debe a que, cuando un hilo empieza a correr, adquiere el GIL. Cuando se queda esperando por una operación de E/S (leer/escribir en disco) o por una operación intensiva de CPU (por ejemplo, multiplicación de vectores o matrices), libera el bloqueo para que otros hilos de ese proceso puedan ejecutarse. Así se evita que varios hilos corran al mismo tiempo.

Detengámonos un momento en el diagrama. Ves una función factorial y dos hilos, 1 y 2: el hilo 1 está bloqueado y el 2 está en espera. Esto significa que solo uno puede acceder a la función. Si suponemos que factorial tarda 2 segundos en completarse, en un caso ideal ambos hilos terminarían en 2 segundos. Sin embargo, en Python, los hilos se ejecutan en serie, no en paralelo.
Los hilos 1 y 2 que llaman a factorial pueden tardar el doble que un único hilo llamando a la función dos veces. Esto también indica que el gestor de memoria controlado por el intérprete no es thread-safe, es decir, que varios hilos no pueden acceder simultáneamente a los mismos datos compartidos.
Así, el GIL (Fuente: Understanding the Python GIL):
-
Limita el uso de hilos.
-
Restringe la ejecución en paralelo.
-
Garantiza que solo un hilo se ejecute en el intérprete a la vez.
-
Simplifica detalles de bajo nivel como la gestión de memoria.
Con GIL se consigue un cómputo cooperativo o multitarea coordinada, en lugar de cómputo en paralelo.
Como se observa en el diagrama, hay tres hilos: inicialmente el hilo 1 está ejecutándose y ha adquirido el GIL; cuando realiza una operación de E/S como leer o escribir, el hilo 1 libera el GIL y lo adquiere el hilo 2. Este ciclo se repite y el GIL va cambiando de hilo alternativamente hasta que todos terminan. Ten en cuenta que cualquier hilo que no tenga el bloqueo y no haya terminado su ejecución permanecerá en espera.
GIL en Python2 vs. Python3

El Global Interpreter Lock en Python 2.7 funciona de forma diferente a Python 3. En una operación puramente intensiva de CPU, el hilo sigue ejecutándose al no haber E/S, con lo que el resto de hilos quedan inactivos o en espera, algo indeseable. Para mitigarlo, Python 2 introduce el concepto de ticks. El GIL comprueba periódicamente el estado de los hilos (si están esperando, en E/S o ejecutándose). No vigila continuamente, sino que usa ticks y hace la comprobación cada 100 ticks.
Un tick es una instrucción de bytecode y, cuando se completan 100 instrucciones de bytecode, el GIL verifica si los hilos están ejecutándose o esperando. Es importante recordar que un tick no está relacionado con el tiempo real: al ser una instrucción de bytecode, cada tick puede tardar más o menos que otro.
Esta comprobación periódica cada 100 ticks es clave, sobre todo en tareas intensivas de CPU, ya que no realizan E/S. Ten en cuenta que puedes modificar el contador de ticks con el módulo integrado sys de Python.
Veamos algunas desventajas de este enfoque:
Si miras este diagrama, verás que los hilos no se ejecutan en paralelo, sino en serie: es el cómputo cooperativo impuesto por el GIL. Además, los hilos deben esperar durante periodos largos. Por ejemplo, el hilo T3 tuvo que esperar a que T1 y T2 liberasen el GIL. En consecuencia, los hilos compiten por el bloqueo y por tiempo de CPU.
Otra desventaja es la contienda entre hilos por adquirir el GIL. Al principio, puede que T1 lo obtenga por prioridad, luego T2, etc. Sin embargo, cuando T3 libera el GIL, notifica al resto y todos vuelven a competir por él. Dicho esto, puedes mitigar el problema ajustando las prioridades de los hilos.
En Python 3 se asigna un tiempo fijo a cada hilo (aprox. 5 ms de ejecución). Así se evita que los hilos pasen hambre de recursos, ya que el tiempo de espera es igual para todos. Los hilos esperarán a ver si el hilo 1 libera el GIL voluntariamente por una operación de E/S o por dormir; si no, se forzará su liberación tras 5 ms. De este modo no hay hambruna de CPU: cada hilo recibe un tiempo similar y se reduce el problema de adquirir el GIL.
Eso sí, la nueva implementación del GIL da más prioridad a tareas intensivas de CPU que a operaciones de E/S. Si un hilo ha liberado el GIL y hay dos esperando, lo adquirirá el que necesite ejecutar una tarea de CPU frente al otro.
Conclusión
¡Enhorabuena por completar el tutorial!
Hemos tratado un tema avanzado con el objetivo de darte una visión teórica de cómo funciona el GIL en Python. Como no hemos visto código sobre GIL e hilos, un buen ejercicio es que experimentes por tu cuenta: crea varios hilos y un método que realice una tarea intensiva de E/S y analiza cómo va cambiando el GIL entre ellos.
No dudes en dejar tus preguntas sobre este tutorial en los comentarios.
Referencias:
Si estás empezando con Python y quieres seguir aprendiendo, haz el curso Introduction to Data Science in Python de DataCamp.
