Multiprocesamiento en Python: Esquivando el GIL

Multiprocesamiento en Python: Esquivando el GIL
Facebook Twitter Flipboard E-mail

En el anterior artículo hicimos una introducción al Global Interpreter Lock (GIL) donde vimos qué es y por qué es necesario en la implementación de CPython.

En este nuevo artículo de multiprocesamiento en Python vamos a aprender a "esquivar" el GIL antes de meternos más a fondo en aspectos más avanzados del multiprocesamiento en Python y las diversas formas que tenemos de utilizar múltiples núcleos de la CPU.

CPython implementa mecanismos para liberar el GIL de forma automática cuando una cierta cantidad de bytecode ha sido ejecutado por el intérprete. Cuando una aplicación de Python se ejecuta, el intérprete compila los módulo y archivos a bytecode que son las instrucciones que finalmente el intérprete ejecuta.

Los archivos Python compilados a bytecode suelen tener la extensión ".pyc" o ".pyo". Una línea de código Python, usualmente será un único bytecode aunque algunas operaciones críticas o importantes pueden ocupar múltiples instrucciones bytecode.

Intérprete, GIL y Bytecode

Cuando el intérprete trabaja con código puro en Python (sin extensiones escritas en C), libera el GIL de forma periódica, usualmente, cada cien instrucciones bytecode. Eso significa que si en nuestro código tenemos una sola línea que ejecute operaciones de extrema complejidad matemática (por ejemplo), el GIL no será liberado hasta que las instrucciones finalicen.

Por supuesto, existe una excepción a esta regla, los módulos programados en C.

Los módulos escritos en C

Cuando programamos un módulo en C, podemos liberar el GIL a voluntad y hacer otras cosas curiosas. Para liberar el GIL mientras nuestro hilo efectúa operaciones bloqueantes se usan las macros Py_BEGIN_ALLOW_THREDS y Py_END_ALLOW_THREADS.

Así podemos esquivar el GIL creando extensiones thread-safe o bien cuando vayamos a efectuar operaciones bloqueantes. Esto nos acerca a tener múltiples hilos en ejecución de forma concurrente.

Algunos programadores intentan liberar el GIL a voluntad de forma regular, esto es posible utilizando llamadas a funciones implementadas en módulos en C que liberen el GIL con Py_BEGIN_ALLOW_THREADS, un módulo que hace eso es por ejemplo el módulo time en la implementación de la función sleep:

...
Py_BEGIN_ALLOW_THREADS
    sleep((int)secs);
Py_END_ALLOW_THREADS
...
Podemos utilizar una llamada a la función sleep del módulo time usando un valor ridículamente bajo sabiendo que liberaremos el GIL y otro hilo será ejecutado antes de volver a la ejecución del hilo donde efectuamos la llamada:
...
time.sleep(0.0000001);
...
Aunque éste método no es lo que podría llamarse "elegante" son muchos los programadores que lo utilizan como una forma de luchar contra la limitación del GIL.

Otras formas de manipular el GIL

El problema, como ya hemos indicado en varias ocasiones, es que el GIL nos impide hacer uso de múltiples procesadores a la vez, dos hilos no pueden ser ejecutados a la vez en el mismo intérprete puesto que solo son ejecutados mediante adquisición/liberación del GIL.

Esto definitivamente encapsula nuestra aplicación en una especie de ejecución "sincronizada de facto" lo cual en ciertos sistemas con más de un procesador puede ser no deseable. Otro mecanismo para manipular el GIL es usar la función del módulo sys setcheckinterval(intervalo).

Esta función fija el valor del "check interval" del intérprete, valor que determina con que frecuencia el intérprete comprueba cosas de forma periódica como el cambio entre hilos y los manejadores de señales. El valor por defecto es 100 lo que significa que se comprueba cada 100 instrucciones virtuales ejecutadas.

Si se usa un valor de cero o menos, se comprobará en cada instrucción lo cual maximizará la sensibilidad y por supuesto la sobrecarga de la aplicación. Otra forma de minimizar los efectos del GIL es ejecutar el intérprete en modo optimizado utilizando el parámetro -O en la línea de comandos.

La verdadera solución

La verdadera y única solución al problema del GIL es extraer nuestras tareas intensivas a módulos en C donde podemos crear hilos y liberar el GIL para que se ejecuten varios hilos de forma concurrente y en paralelo usando múltiples núcleos si queremos implementar una solución a base de hilos.

Otra solución es utilizar el módulo multiprocessing que utiliza la misma API que los threads y permite hacer uso de más de un procesador a la vez y por lo tanto saltarnos la limitación del GIL.

Conclusión

Hoy hemos aprendido unos pocos trucos para minimizar los efectos del GIL de diferentes maneras, en el próximo artículo haremos un pequeño benchmark para ver el resultado de ejecutar una aplicación en CPython utilizando múltiples hilos o utilizando múltiples procesos.


Más Información | API de C para Python Threads
En Genbeta Dev | Multiprocesamiento en Python

Comentarios cerrados
Inicio