Translate

lunes, 9 de diciembre de 2019

Que es Python Global Interpreter Lock?


Antes de empezar necesitamos unos conceptos importantes. Dado que concurrencia y paralelismo son dos conceptos relacionados y mucha gente los confunde debemos empezar marcando las diferencias.

La concurrencia significa, esencialmente, que tanto la tarea A como la B deben suceder independientemente una de otra, y A comienza a ejecutarse, y luego B comienza antes de que A termine.

Hay varias formas diferentes de lograr la concurrencia. Uno de ellos es el paralelismo: tener varias CPU trabajando en las diferentes tareas al mismo tiempo. Pero esa no es la única manera. Otro es por cambio de tareas, que funciona así: la tarea A funciona hasta cierto punto, luego la CPU que trabaja en ella se detiene y cambia a la tarea B, trabaja en ella por un tiempo y luego vuelve a la tarea A. Si los intervalos de tiempo son lo suficientemente pequeños, puede parecerle al usuario que ambas cosas se ejecutan en paralelo, a pesar de que en realidad están siendo procesadas en serie por una CPU multitarea. Se entendió? es como que concurrencia es la sensación de paralelismo, pero no necesariamente es paralelismo. En cambio paralelismo es hacer cosas en paralelo posta.

Y dada esta aclaración, hablemos de GIL,  es el mecanismo utilizado en CPython para impedir que múltiples threads modifiquen los objetos de Python a la vez en una aplicación multi hilo. Esto no evita que tengamos que utilizar primitivas de sincronización en nuestras aplicaciones en Python.

Si en nuestras aplicaciones tenemos varios threads accediendo a una sección de código con datos mutables, tendremos un problema si no utilizamos primitivas de sincronización. Veamos un ejemplo de  thread :

from threading import Thread

def una_funcion:
    print “¡Hola Mundo!”

thread1 = Thread(target=una_funcion)

thread1.start()

thread1.join()

Se puede notar que importamos la clase Thread del módulo threading e instanciamos un nuevo objeto de tipo Thread al que le pasamos la funcion una_funcion. Lo ejecutamos y bloqueamos el hilo de ejecución principal del script hasta que el thread1 regrese de la sección crítica.

Al igual que en otros lenguajes, si queremos que solo un hilo de ejecución haga cambios en los datos de la sección crítica, debemos hacer uso de la clase Lock que nos permite adquirir una sección crítica.

El GIL es un bloqueo a nivel de intérprete. Este bloqueo previene la ejecución de múltiples hilos a la vez en un mismo intérprete de Python. Cada hilo debe esperar a que el GIL sea liberado por otro hilo.

Aunque CPython utiliza el soporte nativo del sistema operativo donde se ejecute a la hora de manejar hilos, y la implementación nativa del sistema operativo permite la ejecución de múltiples hilos de forma simultánea, el intérprete CPython fuerza a los hilos a adquirir el GIL antes de permitirles acceder al intérprete, la pila y puedan así modificar los objetos Python en memoria.

En definitiva, el GIL protege la memoria del intérprete que ejecuta nuestras aplicaciones y no a las aplicaciones en sí. El GIL también mantiene el recolector de basura en un correcto y saneado funcionamiento.

El recolector de basura de Python, como todos los recolectores de basura de diferentes lenguajes de programación, se encarga de liberar la memoria cuando terminamos de usar un objeto. En Python, este mecanismo hace uso de un concepto denominado conteo de referencias.

Cada vez que se hace referencia a un objeto instanciado (un int, una cadena o cualquier otro tipo de objeto nativo o propio) el recolector de basura lo monitorea y suma uno al contador de referencias al objeto. Cuando este número llega al cero, significa que el objeto no está más en uso y el recolector de basura procede a su eliminación de la memoria.

De esta forma no debemos preocuparnos nosotros mismos por liberar la memoria y limpiar los objetos que van a dejar de ser usados como si tenemos que hacer por ejemplo en C o C++. El GIL impide que un thread decremente el valor del conteo de referencia de un objeto a cero mientras otro thread está haciendo uso del mismo. Solo un thread puede acceder a un objeto Python a la vez.

El GIL permite que la implementación de CPython sea extremadamente sencilla a la vez que incrementa la velocidad de ejecución de aplicaciones con un único hilo y la ejecución de aplicaciones multi hilo en sistemas que cuentan con un único procesador. Facilita el mantenimiento del intérprete así como la escritura de módulos y extensiones para el mismo.

Esto es genial, pero también impide la ejecución de múltiples hilos de procesamiento en paralelo en sistemas con múltiples procesadores.

El GIL no es tan malo como puede aparentar a primera vista. Los módulos que realizan tareas de computación intensiva como la compresión o la codificación liberan el GIL mientras operan. También es liberado en todas las operaciones de E/S.

Lo cierto es que si. En 1999 Greg Stein, director de la Apache Software Foundation, y mantenedor de Python y sus librerías desde 1999 al 2003, creó un parche para el intérprete que eliminaba completamente el GIL y añadía bloqueo granular alrededor de operaciones sensibles en el intérprete.

Este parche incrementaba la velocidad de ejecución de aplicaciones multi-hilo pero la decrementaba a la mitad en aplicaciones que ejecutaban un único hilo, lo cual, no era aceptable. Por supuesto esa rama de desarrollo de CPython no tiene ningún tipo de mantenimiento y es hoy inaccesible :(

Por ende con GIL solo tenemos concurrencia y no paralelismo...