Red de conocimiento informático - Problemas con los teléfonos móviles - Cómo Python aprovecha los procesadores multinúcleo

Cómo Python aprovecha los procesadores multinúcleo

El entrelazamiento entre GIL y los hilos de Python

¿Qué es GIL? ¿Cómo afecta a nuestro programa Python? Empecemos con una pregunta. ¿Cuál es el uso de CPU del siguiente programa Python?

# Por favor no imites en el trabajo, es peligroso :) def dead_loop(): while True: passdead_loop()

La respuesta es: el uso de CPU es del 100%, y On ¡Una CPU de un solo núcleo! Esta es una CPU de un solo núcleo, una CPU antigua sin hyper-threading, y en mi CPU de doble núcleo, este bucle infinito solo ocupa la carga de trabajo de uno de los núcleos, es decir, solo ocupa el 50% de la CPU. Entonces, ¿cómo hago para que utilice el 100% de la CPU en una máquina de doble núcleo? La respuesta es fácil de pensar: simplemente use dos subprocesos. ¿No comparten los subprocesos recursos informáticos de la CPU al mismo tiempo? Desafortunadamente, la respuesta es correcta, pero no es tan sencillo de hacer. Además del hilo principal, el siguiente programa también inicia un hilo de bucle infinito

import threadingdef dead_loop(): while True: pass# Inicia un nuevo hilo de bucle infinito t = threading.Thread(target=dead_loop) t.start()# # El hilo principal también entra en un bucle infinito. El hilo principal también entra en un bucle infinito dead_loop()t.join()

Por definición, debería poder hacer esto ocupando los recursos de CPU de dos núcleos, pero en la operación real, no es así. cambia y todavía solo ocupa menos del 50% de los recursos de la CPU. ¿Por qué es esto? ¿Los subprocesos de Python no son subprocesos nativos del sistema operativo? Abrí el monitor del sistema para comprobarlo y descubrí que el proceso de Python que ocupaba el 50% de la CPU se estaba ejecutando en dos subprocesos. ¿Por qué estos dos subprocesos muertos no pueden ocupar todos los recursos de la CPU de doble núcleo?

El mito de GIL: dolor y alegría

El nombre completo de GIL es "Global Interpreter Lock", que significa bloqueo global de intérprete. En CPython, la implementación principal del lenguaje Python, el GIL es un bloqueo de subproceso global real que el intérprete necesita adquirir antes de ejecutar cualquier código Python y liberar el bloqueo cuando encuentra operaciones de E/S. Si se tratara de un programa puramente computacional sin operaciones de E/S, el intérprete liberaría el bloqueo cada 100 operaciones para darle a otro subproceso la oportunidad de ejecutarse (¿este número se puede ajustar con sys.setcheckinterval?). Por lo tanto, aunque la biblioteca de subprocesos de CPython encapsula los subprocesos nativos del sistema operativo, en el proceso de CPython en su conjunto, solo se ejecutará un subproceso y obtendrá el GIL a la vez, y otros subprocesos estarán esperando a que se libere el GIL. Esto explica nuestros resultados experimentales anteriores: aunque hay dos subprocesos muertos y dos núcleos físicos de CPU, debido a las limitaciones de GIL, estos dos subprocesos solo realizan conmutación de tiempo y la utilización total de la CPU sigue siendo ligeramente inferior al 50%.

Parece que el rendimiento de Python es muy bajo y GIL es la razón directa por la que CPython no puede aprovechar el rendimiento físico de múltiples núcleos para acelerar los cálculos. Entonces, ¿por qué está diseñado de esta manera? Supongo que esto sigue siendo un legado de la historia. En la década de 1990, las CPU multinúcleo eran ciencia ficción. Cuando Guido van Rossum creó Python, no podía imaginar que su lenguaje algún día se usaría en CPU que pueden tener más de 1000 núcleos. Por lo tanto, en ese momento, era más simple y económico usar bloqueos globales para manejar múltiples. -Problemas de seguridad del hilo. Simple y satisfactorio, este es el diseño correcto (para el diseño, sólo hay bien o mal, ni bien ni mal). La principal culpa es que el hardware se ha desarrollado demasiado rápido y los dividendos aportados por la Ley de Moore a la industria del software han terminado muy rápidamente.

En menos de 20 años, los programadores no podían esperar hacer que el software antiguo se ejecutara más rápido simplemente actualizando la CPU. En la era de los múltiples núcleos, no hay nada gratis en la programación. Si un programa no puede exprimir el rendimiento informático de cada núcleo mediante la concurrencia, será eliminado. Esto es válido tanto para el software como para los lenguajes. ¿Cómo responde Python?

La solución de Python es simple: mantener el rumbo. En la última versión de Python 3, el GIL todavía existe y no se eliminará por los siguientes motivos:

Para hacer esto, necesita usar el GIL:

El GIL de CPython está diseñado para Proteja todos los intérpretes globales y las variables de estado del entorno. Si elimina el GIL, necesitará múltiples bloqueos más detallados para proteger gran parte del estado global del intérprete. Alternativamente, se puede utilizar un algoritmo sin bloqueo. De cualquier manera, es mucho más difícil ser seguro para múltiples subprocesos que simplemente usar un solo candado con GIL. Con la necesidad de modificar el árbol de código CPython de 20 años de antigüedad y con tantas extensiones de terceros que dependen de GIL, para la comunidad Python era como empezar desde cero.

Incluso si lo hace, probablemente no funcione:

Alguien creó una vez una versión probada de CPython que eliminó el GIL y agregó un bloqueo más detallado. Sin embargo, después de las pruebas reales, para programas de un solo subproceso, el rendimiento de esta versión cae significativamente y su rendimiento superará al de la versión GIL solo después de usar más de un cierto número de CPU físicas. No es de extrañar. Los programas de un solo subproceso no necesitan demasiados bloqueos. En términos de la gestión de bloqueos en sí, bloquear los bloqueos de grano grueso de GIL es definitivamente más rápido que gestionar una gran cantidad de bloqueos de grano fino. Y ahora la gran mayoría de los programas de Python son de un solo subproceso. Además, en términos de requisitos, Python no se utiliza para su rendimiento computacional. Incluso si pudiera aprovechar múltiples núcleos, su rendimiento no estaría a la par con C/C++. Esto no va en contra del hecho de que eliminar minuciosamente el GIL de la ecuación ralentiza la mayoría de los programas.

¿Python realmente está renunciando a la era multinúcleo porque es difícil de cambiar? De hecho, la razón más importante para no cambiarlo es que funciona bien sin cambiarse.

Otras armas mágicas

Entonces, en la era multinúcleo, además de cancelar el GIL, ¿existe realmente alguna otra forma de mantener vivo a Python? Volvamos a la pregunta original de este artículo: ¿Cómo consigo que este script Python congelado utilice el 100% de la CPU en una máquina de doble núcleo? La respuesta más simple es: ¡ejecute dos programas Python que fallan! En otras palabras, dos procesos de Python ocupan cada uno un núcleo de CPU. De hecho, el multiprocesamiento también es una excelente manera de aprovechar múltiples CPU. Sin embargo, los procesos tienen espacios de direcciones de memoria independientes y la comunicación entre sí es mucho más complicada que la de subprocesos múltiples. Con este fin, Python introdujo una nueva biblioteca estándar multiproceso en 2.6, que simplifica los programas Python multiproceso a un nivel similar al de subprocesos múltiples y reduce en gran medida la vergüenza de no poder utilizar núcleos múltiples, que es lo que GIL trae pregunta.

Este es sólo un enfoque; si no desea utilizar una solución pesada como el multiprocesamiento, existe una opción más radical, que es abandonar Python y cambiar a C/C++. Normalmente, los programas computacionalmente intensivos se escriben en C y se integran en scripts de Python mediante extensiones como el módulo NumPy. Las extensiones le permiten crear subprocesos nativos en C sin bloquear el GIL, utilizando así plenamente los recursos computacionales de la CPU. Sin embargo, escribir extensiones de Python siempre es complicado. Afortunadamente, Python tiene otro mecanismo para interoperar con módulos C: ctypes

Utilice ctypes para omitir el GIL

Ctypes, a diferencia de las extensiones de Python, permite a Python llamar directamente a C dinámico. Cualquier función exportada en la biblioteca. Todo lo que tienes que hacer es escribir código Python en ctypes.

Lo bueno es que ctypes libera el GIL antes de llamar a la función C, por lo que podemos usar ctypes y la biblioteca C para permitir que Python aproveche al máximo la potencia informática del núcleo físico. Verifiquémoslo. Esta vez usamos el lenguaje C para escribir una función de bucle infinito extern "C"{ ?void DeadLoop() ?{ while (true ?}}

Compile el código C anterior. y generar la biblioteca dinámica libdead_loop.so (libdead_loop.so en sistemas Windows). (?dead_loop.dll en Windows)

Luego tienes que cargar la biblioteca en Python usando ctypes y llamar a ?void DeadLoop(??????? ??????????? ??????????????????). DeadLoopfrom ctypes import *from threading import Threadlib = cdll.LoadLibrary("libdead_loop.so")t = Thread(target=lib.DeadLoop)t.start( )lib.DeadLoop()

Veamos de nuevo esto time Mirando el monitor del sistema, el proceso del intérprete de Python tiene dos subprocesos en ejecución y la CPU de doble núcleo está completamente ocupada, por lo que ctypes es realmente muy poderoso. Tenga en cuenta que ctypes libera el GIL antes de llamar a la función C. Sin embargo, el intérprete de Python aún bloquea el GIL al ejecutar cualquier código Python. Si usa código Python como devolución de llamada a una función C, el GIL seguirá apareciendo cada vez que se ejecute el método de devolución de llamada de Python. Por ejemplo, aquí hay un ejemplo: extern "C"{ typedef void Callback(); ?void Call(Callback* callback) ?{ callback() ?}}

from ctypes import *from threading import Threaddef dead_loop(): mientras que True: passlib = cdll.LoadLibrary("libcall.so") Devolución de llamada = CFUNCTYPE (Ninguno)devolución de llamada = Devolución de llamada(dead_loop)t = Thread(target=lib.Call, args=(devolución de llamada,))t . start()lib.Call(callback)

Observe la diferencia con el ejemplo anterior. Tenga en cuenta la diferencia con el ejemplo anterior, en este caso, el bucle muerto ocurre en el código Python (¿DeadLoop? Después de ejecutar este ejemplo, encontrará que el uso de la CPU todavía está ligeramente por debajo del 50% y el GIL comienza a funcionar nuevamente

De hecho, podemos ver una aplicación de ctypes en el ejemplo anterior, que consiste en escribir casos de prueba automáticos en Python y llamar directamente a la interfaz del módulo para realizar pruebas de caja negra en el módulo C. pruebas de seguridad de subprocesos múltiples de la interfaz C del módulo, ctypes puede hacer esto

Conclusión

Aunque la biblioteca de subprocesos de CPython encapsula los subprocesos nativos del sistema operativo, pero la existencia de. GIL evita que los subprocesos múltiples aprovechen la potencia informática de múltiples núcleos de CPU. Ahora que Python tiene multiprocesamiento, extensiones C y tipos, es más que suficiente para enfrentar los desafíos de la era de múltiples núcleos. si el GIL ha sido cortado, ¿verdad?