¿Qué es el locking de intérprete global (GIL) en CPython?

¿Qué es un locking de intérprete global y por qué es un problema?

Se ha hecho mucho ruido al eliminar el GIL de Python, y me gustaría entender por qué es tan importante. Nunca he escrito un comstackdor ni un intérprete, así que no sea frugal con los detalles, probablemente los necesitaré para comprender.

El GIL de Python está pensado para serializar el acceso a las partes internas del intérprete desde diferentes hilos. En los sistemas de varios núcleos, significa que varios subprocesos no pueden hacer uso eficaz de múltiples núcleos. (Si la GIL no condujera a este problema, a la mayoría de las personas no les importaría la GIL, solo se plantea como un problema debido a la creciente prevalencia de los sistemas de múltiples núcleos). Si desea comprenderlo en detalle, Puedes ver este video o mirar este conjunto de diapositivas . Puede que sea demasiada información, pero luego pediste detalles 🙂

Tenga en cuenta que GIL de Python solo es realmente un problema para CPython, la implementación de referencia. Jython y IronPython no tienen un GIL. Como desarrollador de Python, generalmente no te encuentras con GIL a menos que estés escribiendo una extensión en C. Los escritores de extensiones C necesitan liberar GIL cuando sus extensiones bloquean la E / S, para que otros subprocesos en el proceso de Python tengan la oportunidad de ejecutarse.

Supongamos que tiene múltiples hilos que realmente no se tocan entre los datos. Aquellos deben ejecutarse lo más independientemente posible. Si tiene un “locking global” que debe adquirir para (por ejemplo) llamar a una función, eso puede terminar como un cuello de botella. Puede terminar no obteniendo mucho beneficio de tener múltiples hilos en primer lugar.

Para ponerlo en una analogía del mundo real: imagine a 100 desarrolladores trabajando en una empresa con una sola taza de café. La mayoría de los desarrolladores pasaban el tiempo esperando el café en lugar de la encoding.

Nada de esto es específico de Python. No conozco los detalles de para qué necesitaba Python un GIL en primer lugar. Sin embargo, espero que te haya dado una mejor idea del concepto general.

Primero entendamos lo que proporciona la Python GIL:

Cualquier operación / instrucción se ejecuta en el intérprete. GIL se asegura de que el intérprete esté sujeto a un solo hilo en un momento determinado del tiempo . Y su progtwig de Python con múltiples hilos trabaja en un solo intérprete. En cualquier momento particular del tiempo, este intérprete es sostenido por un solo hilo. Significa que solo el hilo que contiene al intérprete se está ejecutando en cualquier momento del tiempo .

Ahora por qué es eso un problema:

Su máquina podría tener múltiples núcleos / procesadores. Y los múltiples núcleos permiten que múltiples hilos se ejecuten simultáneamente, es decir, múltiples hilos podrían ejecutarse en cualquier momento particular del tiempo. . Pero como el intérprete está sujeto por un solo hilo, otros hilos no están haciendo nada a pesar de que tienen acceso a un núcleo. Por lo tanto, no está obteniendo ninguna ventaja proporcionada por múltiples núcleos porque en cualquier momento solo se está utilizando un solo núcleo, que es el núcleo que está utilizando el hilo que actualmente tiene el intérprete. Por lo tanto, su progtwig tardará tanto en ejecutarse como si se tratara de un progtwig de un solo hilo.

Sin embargo, las operaciones de locking o de ejecución prolongada, como la E / S, el procesamiento de imágenes y el procesamiento de números NumPy, ocurren fuera de la GIL. Tomado de aquí . Por lo tanto, para tales operaciones, una operación multiproceso aún será más rápida que una operación de un solo hilo a pesar de la presencia de GIL. Entonces, GIL no es siempre un cuello de botella.

Edit: GIL es un detalle de implementación de CPython. IronPython y Jython no tienen GIL, por lo que en ellos debería ser posible un progtwig verdaderamente multiproceso, pensé que nunca he usado PyPy y Jython y no estoy seguro de esto.

Python no permite subprocesos múltiples en el verdadero sentido de la palabra. Tiene un paquete de subprocesos múltiples, pero si desea realizar varios subprocesos para acelerar su código, por lo general no es una buena idea usarlo. Python tiene una construcción llamada Global Intérprete Lock (GIL).

https://www.youtube.com/watch?v=ph374fJqFPE

La GIL se asegura de que solo uno de tus ‘hilos’ pueda ejecutarse a la vez. Un hilo adquiere el GIL, hace un poco de trabajo, luego pasa el GIL al siguiente hilo. Esto sucede muy rápidamente, por lo que, para el ojo humano, puede parecer que tus hilos se ejecutan en paralelo, pero en realidad solo están tomando turnos utilizando el mismo núcleo de CPU. Todo este paso GIL agrega sobrecarga a la ejecución. Esto significa que si desea que su código se ejecute más rápido, usar el paquete de subprocesos a menudo no es una buena idea.

Hay razones para usar el paquete de hilos de Python. Si desea ejecutar algunas cosas simultáneamente, y la eficiencia no es una preocupación, entonces es totalmente bueno y conveniente. O si está ejecutando un código que necesita esperar algo (como algunos IO), entonces podría tener mucho sentido. Pero la biblioteca de subprocesos no le permitirá utilizar núcleos de CPU adicionales.

Los subprocesos múltiples pueden ser subcontratados al sistema operativo (haciendo multiprocesamiento), alguna aplicación externa que llame a su código Python (por ejemplo, Spark o Hadoop), o algún código que su código de Python llame (por ejemplo: usted podría tener su Python código llamar a una función de C que hace el costoso multi-threaded cosas).

Cada vez que dos hilos tienen acceso a la misma variable, tiene un problema. En C ++, por ejemplo, la forma de evitar el problema es definir un locking de exclusión mutua para evitar que dos subprocesos, digamos, ingresen al colocador de un objeto al mismo tiempo.

El subprocesamiento múltiple es posible en python, pero dos subprocesos no pueden ejecutarse al mismo tiempo con una granularidad más fina que una instrucción de python. El hilo en ejecución está obteniendo un locking global llamado GIL.

Esto significa que si comienza a escribir un código de multiproceso para aprovechar su procesador multinúcleo, su rendimiento no mejorará. La solución habitual consiste en ir multiproceso.

Tenga en cuenta que es posible liberar la GIL si está dentro de un método que escribió en C, por ejemplo.

El uso de un GIL no es inherente a Python sino a algunos de sus intérpretes, incluido el CPython más común. (# editado, ver comentario)

El problema de GIL sigue siendo válido en Python 3000.

Documentación de Python 3.7

También me gustaría resaltar la siguiente cita de la documentación de threading Python :

Detalle de la implementación de CPython: en CPython, debido al locking global de intérpretes, solo un hilo puede ejecutar el código de Python a la vez (aunque ciertas bibliotecas orientadas al rendimiento pueden superar esta limitación). Si desea que su aplicación haga un mejor uso de los recursos computacionales de las máquinas de múltiples núcleos, se recomienda utilizar multiprocessing o concurrent.futures.ProcessPoolExecutor . Sin embargo, el subproceso sigue siendo un modelo adecuado si desea ejecutar varias tareas enlazadas a E / S simultáneamente.

Esto se vincula a la entrada del Glosario para global interpreter lock que explica que GIL implica que el paralelismo de subprocesos en Python no es adecuado para tareas vinculadas a la CPU :

El mecanismo utilizado por el intérprete de CPython para asegurar que solo un hilo ejecute el código de bytes de Python a la vez. Esto simplifica la implementación de CPython al hacer que el modelo de objeto (incluidos los tipos críticos incorporados, como dict), sea seguro contra el acceso concurrente. El locking de todo el intérprete facilita que el intérprete sea multiproceso, a expensas de gran parte del paralelismo que ofrecen las máquinas multiprocesador.

Sin embargo, algunos módulos de extensión, ya sean estándar o de terceros, están diseñados para liberar la GIL cuando realizan tareas de computación intensivas como la compresión o el hashing. Además, la GIL siempre se libera cuando se hace E / S.

Los esfuerzos anteriores para crear un intérprete de “subprocesos libres” (uno que bloquea los datos compartidos con una granularidad mucho más precisa) no han tenido éxito porque el rendimiento se vio afectado en el caso común de un solo procesador. Se cree que superar este problema de rendimiento haría que la implementación fuera mucho más complicada y, por lo tanto, más costosa de mantener.

Esta cita también implica que los dictados y, por tanto, la asignación también son seguros para subprocesos en CPython:

  • ¿Es la asignación de variables de Python atómica?
  • Seguridad del hilo en el diccionario de Python

A continuación, los documentos para el paquete de multiprocessing explican cómo supera la GIL mediante el proceso de generación al tiempo que expone una interfaz similar a la de los threading :

el multiprocesamiento es un paquete que admite procesos de generación utilizando una API similar al módulo de subprocesos. El paquete de multiprocesamiento ofrece tanto la concurrencia local como la remota, de manera efectiva, de forma paralela al locking global de intérpretes mediante el uso de subprocesos en lugar de subprocesos. Debido a esto, el módulo de multiprocesamiento le permite al progtwigdor aprovechar al máximo los múltiples procesadores en una máquina determinada. Se ejecuta en Unix y Windows.

Y los documentos para concurrent.futures.ProcessPoolExecutor explican que utiliza el multiprocessing como backend:

La clase ProcessPoolExecutor es una subclase Executor que utiliza un grupo de procesos para ejecutar llamadas de forma asíncrona. ProcessPoolExecutor utiliza el módulo de multiprocesamiento, que le permite pasar por alto el locking global de intérpretes, pero también significa que solo se pueden ejecutar y devolver los objetos extraíbles.

que debe contrastarse con la otra clase base ThreadPoolExecutor que usa subprocesos en lugar de procesos

ThreadPoolExecutor es una subclase Executor que utiliza un conjunto de subprocesos para ejecutar llamadas de forma asíncrona.

de lo cual concluimos que ThreadPoolExecutor solo es adecuado para tareas de E / S enlazadas, mientras que ProcessPoolExecutor también puede manejar tareas vinculadas a la CPU.

La siguiente pregunta pregunta por qué existe GIL en primer lugar: ¿Por qué el locking global de intérpretes?

Proceso vs experimentos de hilo

En Multiprocessing vs Threading Python he realizado un análisis experimental de procesos vs hilos en Python.

Vista previa rápida de los resultados:

introduzca la descripción de la imagen aquí

¿Por qué Python (CPython y otros) usa el GIL?

De http://wiki.python.org/moin/GlobalInterpreterLock

En CPython, el locking global del intérprete, o GIL, es un mutex que impide que varios subprocesos nativos ejecuten códigos de byte de Python a la vez. Este locking es necesario principalmente porque la administración de memoria de CPython no es segura para subprocesos.

¿Cómo eliminarlo de Python?

Al igual que Lua, tal vez Python podría iniciar múltiples máquinas virtuales, pero python no hace eso, supongo que debería haber otras razones.

En Numpy o en alguna otra biblioteca extendida de python, a veces, la liberación de GIL a otros subprocesos podría boost la eficiencia de todo el progtwig.

Quiero compartir un ejemplo del libro multihilo para efectos visuales. Así que aquí hay una situación clásica de locking muerto.

 static void MyCallback(const Context &context){ Auto lock(GetMyMutexFromContext(context)); ... EvalMyPythonString(str); //A function that takes the GIL ... } 

Ahora considere los eventos en la secuencia que resultan en un candado.

 ╔═══╦════════════════════════════════════════╦══════════════════════════════════════╗ ║ ║ Main Thread ║ Other Thread ║ ╠═══╬════════════════════════════════════════╬══════════════════════════════════════╣ ║ 1 ║ Python Command acquires GIL ║ Work started ║ ║ 2 ║ Computation requested ║ MyCallback runs and acquires MyMutex ║ ║ 3 ║ ║ MyCallback now waits for GIL ║ ║ 4 ║ MyCallback runs and waits for MyMutex ║ waiting for GIL ║ ╚═══╩════════════════════════════════════════╩══════════════════════════════════════╝