Red de conocimiento informático - Problemas con los teléfonos móviles - ¿Qué es el GIL de Python y cuál es su rendimiento multiproceso?

¿Qué es el GIL de Python y cuál es su rendimiento multiproceso?

Qué es GIL

Lo primero que hay que dejar claro es que GIL

no es una característica de Python, se introduce al implementar Python. analizador (CPython) un concepto. Al igual que C es un conjunto de estándares de lenguaje (gramática), pero se puede compilar en código ejecutable utilizando diferentes compiladores. Compiladores famosos como GCC, INTEL

C, Visual

C, etc. Lo mismo ocurre con Python. El mismo fragmento de código se puede ejecutar a través de diferentes entornos de ejecución de Python, como CPython, PyPy y Psyco. Por ejemplo, JPython no tiene GIL. Sin embargo, CPython es el entorno de ejecución de Python predeterminado en la mayoría de los entornos. Por lo tanto, en el concepto de muchas personas, CPython es Python, y dan por sentado que GIL se atribuye a un defecto del lenguaje Python. Así que dejemoslo claro aquí: GIL no es una característica de Python, y Python no necesita depender de GIL en absoluto

Entonces, ¿qué es GIL en la implementación de CPython? El nombre completo de GIL es Global Interpreter Lock. Para evitar engaños, echemos un vistazo a la explicación oficial:

En CPython, el bloqueo global de intérprete, o GIL, es un mutex que

<. p>evita que múltiples subprocesos nativos ejecuten códigos de bytes de Python

al mismo tiempo. Este bloqueo es necesario principalmente porque la administración de memoria de CPython

no es segura para subprocesos (sin embargo, dado que GIL existe, otras características

han pasado a depender de las garantías que impone).

Bueno, ¿no se ve mal? ¡Un Mutex que evita que varios subprocesos ejecuten código de máquina simultáneamente parece ser un bloqueo global que parece un error a primera vista! No te preocupes, lo analizaremos lentamente a continuación.

Por qué existe GIL

Debido a limitaciones físicas, la competencia entre fabricantes de CPU en la frecuencia del núcleo ha sido reemplazada por la competencia multinúcleo. Para utilizar de manera más efectiva el rendimiento de los procesadores multinúcleo, ha surgido la programación multiproceso, y con ella surge la dificultad de la coherencia de los datos y la sincronización del estado entre los subprocesos. Incluso el caché dentro de la CPU no es una excepción. Para resolver eficazmente el problema de la sincronización de datos entre múltiples cachés, varios fabricantes han dedicado mucho esfuerzo, lo que inevitablemente provoca una cierta pérdida de rendimiento.

Por supuesto, Python no puede escapar. Para aprovechar múltiples núcleos, Python comenzó a admitir subprocesos múltiples. La forma más sencilla de resolver la integridad de los datos y la sincronización del estado entre varios subprocesos es, naturalmente, bloquear. Entonces existe el bloqueo súper grande de GIL, y cuando cada vez más desarrolladores de base de código aceptan esta configuración, comienzan a depender en gran medida de esta característica (es decir, los objetos internos predeterminados de Python son seguros para subprocesos y no hay necesidad de La implementación tiene en cuenta bloqueos de memoria adicionales y operaciones de sincronización).

Poco a poco se descubrió que este método de implementación era doloroso e ineficiente. Pero cuando todos intentaron dividir y eliminar el GIL, descubrieron que una gran cantidad de desarrolladores de códigos de biblioteca han confiado en gran medida en el GIL y es muy difícil eliminarlo.

¿Qué tan difícil es? Como analogía, para un "proyecto pequeño" como MySQL, se necesitaron entre 5,5 y 5,6 para dividir el gran bloqueo de Buffer

Pool

Mutex en pequeños bloqueos. Varias versiones principales de 5.7. Duró casi 5 años y aún continúa. MySQL, un producto con soporte de la empresa y un equipo de desarrollo fijo detrás, está pasando por un momento tan difícil, sin mencionar un equipo altamente comunitario de desarrolladores centrales y contribuyentes de código como Python.

En pocas palabras, la existencia de GIL se debe más a razones históricas. Si tuviéramos que hacerlo todo de nuevo, todavía tendríamos que enfrentarnos al problema de los subprocesos múltiples, pero al menos sería más elegante que el enfoque GIL actual.

El impacto de GIL

De la introducción anterior y la definición oficial, GIL es sin duda un bloqueo exclusivo global. No hay duda de que la existencia de bloqueos globales tendrá un gran impacto en la eficiencia de los subprocesos múltiples. Es casi como si Python fuera un programa de un solo subproceso. Entonces los lectores dirán que mientras se libere el bloqueo global, la eficiencia no será mala. Siempre que se pueda liberar GIL al realizar operaciones de IO que requieren mucho tiempo, aún se puede mejorar la eficiencia operativa. En otras palabras, no importa cuán malo sea, no será peor que la eficiencia de un solo hilo. Esto es cierto en teoría, pero ¿en la práctica? Python es peor de lo que piensas.

Comparemos la eficiencia de Python en subprocesos múltiples y en subprocesos únicos. El método de prueba es muy simple: una función de contador que se repite 100 millones de veces. Uno se ejecuta dos veces a través de un solo subproceso y el otro se ejecuta a través de varios subprocesos. Finalmente compare el tiempo total de ejecución. El entorno de prueba es un Mac

pro de doble núcleo. Nota: Para reducir el impacto de la pérdida de rendimiento de la propia biblioteca de subprocesos en los resultados de la prueba, el código de un solo subproceso aquí también utiliza subprocesos. Simplemente ejecútelo dos veces secuencialmente para simular un solo hilo.

Subproceso único (single_thread.py) para ejecución secuencial

#! /usr/bin/python

desde subprocesos import Thread

tiempo de importación

def my_counter():

i = 0

for _ in range(100000000):

i = i 1

devuelve Verdadero

def main():

thread_array = {}

start_time = time.time()

para tid en el rango(2):

t = Thread(target=my_counter)

t.start()

thread_array[tid] = t

para i en el rango(2):

thread_array[i].join()

end_time = time.time()

print("Tiempo total: {}".format(end_time - start_time))

if __name__ == '__main__':

main()

Dos subprocesos concurrentes ejecutados simultáneamente (multi_thread.py)

#! /usr/bin/python

desde el subproceso importar subproceso

tiempo de importación

p>

def my_counter():

i = 0

para _ en el rango(100000000):

i = i 1

return True

def main():

thread_array = {}

start_time = time.time()

para tid en rango(2):

t = Thread(target=my_counter)

t.start()

thread_array[tid] = t

para i en el rango(2):

thread_array[i].join()

end_time = time.time()

print( "Tiempo total: {}".format(end_time - start_time))

if __name__ == '__main__':

main()