Un ejemplo multi-threading del python GIL.

He leído un poco acerca de cuán “malo” es este negocio de GIL en Python al escribir código de múltiples subprocesos, pero nunca he visto un ejemplo. ¿Podría alguien darme un ejemplo básico de cuándo la GIL causa problemas al usar subprocesos?

¡Gracias!

Una de las razones principales para el subprocesamiento múltiple es que un progtwig puede aprovechar múltiples CPU (y / o múltiples núcleos en una CPU) para calcular más operaciones por segundo. Pero en Python, la GIL significa que incluso si tienes múltiples subprocesos chugeando simultáneamente en un cómputo, solo uno de esos subprocesos se ejecutará en un momento dado, porque todos los demás estarán bloqueados, esperando la adquisición del intérprete global bloquear. Eso significa que el progtwig Python de subprocesos múltiples en realidad será más lento que la versión de un solo subproceso, en lugar de hacerlo más rápido, ya que solo se ejecuta un subproceso a la vez, además, existe la sobrecarga de contabilidad en la que se incurre al obligar a cada subproceso a esperar, adquirir y luego renuncie a la GIL (estilo round-robin) cada pocos milisegundos.

Para demostrar esto, aquí hay un script Python de juguete que genera un número específico de subprocesos, y luego, como su “cálculo”, cada subproceso incrementa continuamente un contador hasta que hayan transcurrido 5 segundos. Al final, el hilo principal cuenta el número total de contraimpuestos que se produjo e imprime el total, para darnos una medida de cuánto “trabajo” se realizó durante el período de 5 segundos.

import threading import sys import time numSecondsToRun = 5 class CounterThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) self._counter = 0 self._endTime = time.time() + numSecondsToRun def run(self): # Simulate a computation on the CPU while(time.time() < self._endTime): self._counter += 1 if __name__ == "__main__": if len(sys.argv) < 2: print "Usage: python counter 5" sys.exit(5) numThreads = int(sys.argv[1]) print "Spawning %i counting threads for %i seconds..." % (numThreads, numSecondsToRun) threads = [] for i in range(0,numThreads): t = CounterThread() t.start() threads.append(t) totalCounted = 0 for t in threads: t.join() totalCounted += t._counter print "Total amount counted was %i" % totalCounted 

.... y aquí están los resultados que obtengo en mi computadora (que es un Mac Mini de doble núcleo con hipervínculos habilitados, FWIW):

 $ python counter.py 1 Spawning 1 counting threads for 5 seconds... Total amount counted was 14210740 $ python counter.py 2 Spawning 2 counting threads for 5 seconds... Total amount counted was 10398956 $ python counter.py 3 Spawning 3 counting threads for 5 seconds... Total amount counted was 10588091 $ python counter.py 4 Spawning 4 counting threads for 5 seconds... Total amount counted was 11091197 $ python counter.py 5 Spawning 5 counting threads for 5 seconds... Total amount counted was 11130036 $ python counter.py 6 Spawning 6 counting threads for 5 seconds... Total amount counted was 10771654 $ python counter.py 7 Spawning 7 counting threads for 5 seconds... Total amount counted was 10464226 

Observe cómo se logró el mejor rendimiento en la primera iteración (donde solo se generó un único hilo de trabajo); la productividad de conteo disminuyó considerablemente cuando se ejecutaba más de un subproceso a la vez. Esto muestra cómo GIL paraliza el rendimiento de subprocesos múltiples en Python: el mismo progtwig escrito en C (o cualquier otro lenguaje sin GIL) mostraría un rendimiento mucho mejor con más subprocesos en ejecución, no peor (hasta que el número de subprocesos de trabajo coincida con el Número de núcleos en el hardware, por supuesto).

Sin embargo, esto no significa que el subprocesamiento múltiple sea completamente inútil en Python, aún es útil en los casos en que la mayoría o todos los subprocesos están bloqueados esperando la E / S en lugar de estar enlazados a la CPU. Esto se debe a que un subproceso de Python que está bloqueado en espera de E / S no mantiene bloqueada la GIL mientras espera, por lo que durante ese tiempo otros subprocesos aún pueden ejecutarse. Sin embargo, si necesita poner en paralelo una tarea intensiva en computación (p. Ej., Trazado de rayos o cómputo de todos los dígitos de Pi o descifrado de código o similar), entonces querrá usar múltiples procesos en lugar de múltiples hilos, o usar un idioma diferente que no tiene un GIL.