threading thread start setdaemon lock example ejemplos acquire python multithreading locking

thread - ¿Los bloqueos son innecesarios en el código de Python de subprocesos múltiples debido al GIL?



threading python 3 (9)

Aún necesita usar bloqueos (su código podría interrumpirse en cualquier momento para ejecutar otro hilo y esto puede causar inconsistencias en los datos). El problema con GIL es que evita que el código de Python use más núcleos al mismo tiempo (o múltiples procesadores si están disponibles).

Si confía en una implementación de Python que tiene un bloqueo de intérprete global (es decir, CPython) y escribe código multiproceso, ¿realmente necesita bloqueos?

Si el GIL no permite que se ejecuten varias instrucciones en paralelo, ¿no sería innecesario proteger los datos compartidos?

lo siento si esta es una pregunta tonta, pero es algo que siempre me he preguntado sobre Python en máquinas multiprocesador / núcleo.

Lo mismo se aplicaría a cualquier implementación de otro idioma que tenga un GIL.


Aún necesitará bloqueos si comparte estado entre hilos. GIL solo protege al intérprete internamente. Aún puede tener actualizaciones inconsistentes en su propio código.

Por ejemplo:

#!/usr/bin/env python import threading shared_balance = 0 class Deposit(threading.Thread): def run(self): for _ in xrange(1000000): global shared_balance balance = shared_balance balance += 100 shared_balance = balance class Withdraw(threading.Thread): def run(self): for _ in xrange(1000000): global shared_balance balance = shared_balance balance -= 100 shared_balance = balance threads = [Deposit(), Withdraw()] for thread in threads: thread.start() for thread in threads: thread.join() print shared_balance

Aquí, su código se puede interrumpir entre leer el estado compartido ( balance = shared_balance ) y escribir el resultado cambiado de nuevo ( shared_balance = balance ), causando una actualización perdida. El resultado es un valor aleatorio para el estado compartido.

Para que las actualizaciones sean consistentes, los métodos de ejecución necesitarían bloquear el estado compartido alrededor de las secciones de lectura-modificación-escritura (dentro de los bucles) o tener alguna forma de detectar cuándo el estado compartido había cambiado desde que se leyó .



El bloqueo de intérprete global evita que los subprocesos accedan al intérprete simultáneamente (por lo tanto CPython solo utiliza un núcleo). Sin embargo, según tengo entendido, los hilos todavía están interrumpidos y programados de forma preventiva , lo que significa que todavía necesita bloqueos en las estructuras de datos compartidas, para que sus hilos no pisen los dedos del otro.

La respuesta que he encontrado una y otra vez es que el multihilo en Python no suele valer la pena, debido a esto. He escuchado cosas buenas sobre el proyecto PyProcessing , que hace que ejecutar múltiples procesos sea tan simple como multithreading, con estructuras de datos compartidas, colas, etc. (PyProcessing se introducirá en la biblioteca estándar del próximo Python 2.6 como el módulo de multiprocessing .) Esto lo lleva al GIL, ya que cada proceso tiene su propio intérprete.


Esta publicación describe el GIL en un nivel bastante alto:

De particular interés son estas citas:

Cada diez instrucciones (este predeterminado se puede cambiar), el núcleo libera el GIL para el hilo actual. En ese punto, el SO elige un hilo de todos los hilos que compiten por el candado (posiblemente eligiendo el mismo hilo que acaba de lanzar el GIL - usted no tiene ningún control sobre qué hilo se elige); ese hilo adquiere el GIL y luego se ejecuta para otros diez bytecodes.

y

Tenga en cuenta que GIL solo restringe el código de Python puro. Las extensiones (bibliotecas externas de Python generalmente escritas en C) se pueden escribir para liberar el bloqueo, lo que permite que el intérprete de Python se ejecute por separado de la extensión hasta que la extensión vuelva a adquirir el bloqueo.

Parece que GIL solo proporciona menos instancias posibles para un cambio de contexto, y hace que los sistemas multi-core / processor se comporten como un único núcleo, con respecto a cada instancia de Python Intérprete, así que sí, aún necesita usar mecanismos de sincronización.


No, el GIL solo protege las partes internas de Python de múltiples hilos que alteran su estado. Este es un nivel muy bajo de bloqueo, suficiente solo para mantener las propias estructuras de Python en un estado consistente. No cubre el bloqueo del nivel de la aplicación que deberá hacer para cubrir la seguridad del hilo en su propio código.

La esencia del bloqueo es garantizar que un solo bloque de código se ejecute solo con un hilo. El GIL aplica esto para bloquear el tamaño de un solo bytecode, pero generalmente quiere que el bloqueo abarque un bloque de código más grande que este.


Piénsalo de esta manera:

En una sola computadora con procesador, el multihilo sucede al suspender un hilo y comenzar otro lo suficientemente rápido como para hacer que parezca que se está ejecutando al mismo tiempo. Esto es como Python con GIL: solo un hilo se está ejecutando.

El problema es que el hilo se puede suspender en cualquier lugar, por ejemplo, si quiero calcular b = (a + b) * 3, esto podría producir instrucciones como las siguientes:

1 a += b 2 a *= 3 3 b = a

Ahora, digamos que se está ejecutando en un subproceso y ese subproceso se suspende después de la línea 1 o 2 y luego se inicia otro subproceso y se ejecuta:

b = 5

Luego, cuando el otro hilo se reanuda, b se sobreescribe con los valores computados, lo que probablemente no sea el esperado.

Entonces puede ver que aunque no se estén ejecutando REALMENTE al mismo tiempo, aún necesita bloqueo.


Se necesitan bloqueos. Trataré de explicar por qué son necesarios.

Cualquier operación / instrucción se ejecuta en el intérprete. GIL garantiza que el intérprete se mantenga en un único hilo en un instante particular . Y su programa con múltiples hilos funciona en un solo intérprete. En un instante particular, este intérprete está en un solo hilo. Significa que solo el hilo que contiene el intérprete se está ejecutando en cualquier instante de tiempo.

Supongamos que hay dos hilos, digamos t1 y t2, y ambos quieren ejecutar dos instrucciones que leen el valor de una variable global y la incrementan.

#increment value global var read_var = var var = read_var + 1

Como se indicó anteriormente, GIL solo garantiza que dos subprocesos no pueden ejecutar una instrucción simultáneamente, lo que significa que ambos subprocesos no pueden ejecutar read_var = var en un instante particular de tiempo. Pero pueden ejecutar instrucciones una tras otra y aún así puede tener problemas. Considera esta situación:

  • Supongamos que read_var es 0.
  • GIL está retenido por el hilo t1.
  • t1 ejecuta read_var = var . Entonces, read_var en t1 es 0. GIL solo se asegurará de que esta operación de lectura no se ejecute para ningún otro hilo en este instante.
  • GIL se le da al hilo t2.
  • t2 ejecuta read_var = var . Pero read_var sigue siendo 0. Entonces, read_var en t2 es 0.
  • GIL se le da a t1.
  • t1 ejecuta var = read_var+1 y var se convierte en 1.
  • GIL se le da a t2.
  • t2 piensa read_var = 0, porque eso es lo que lee.
  • t2 ejecuta var = read_var+1 y var se convierte en 1.
  • Nuestra expectativa era que var se convierta en 2.
  • Por lo tanto, se debe usar un bloqueo para mantener tanto la lectura como el incremento como una operación atómica.
  • La respuesta de Harris lo explica a través de un ejemplo de código.

Un poco de actualización del ejemplo de Will Harris:

class Withdraw(threading.Thread): def run(self): for _ in xrange(1000000): global shared_balance if shared_balance >= 100: balance = shared_balance balance -= 100 shared_balance = balance

Ponga una declaración de verificación de valor en la retirada y ya no veo el negativo y las actualizaciones parecen consistentes. Mi pregunta es:

Si GIL impide que solo se pueda ejecutar un hilo en cualquier momento atómico, ¿dónde estaría el valor obsoleto? Si no hay valor obsoleto, ¿por qué necesitamos bloqueo? (Suponiendo que solo hablamos del código python puro)

Si entiendo correctamente, la verificación de la condición anterior no funcionaría en un entorno real de enhebrado. Cuando se ejecutan más de un subproceso al mismo tiempo, se puede crear un valor obsoleto, por lo tanto, la incoherencia del estado compartido, entonces realmente necesita un bloqueo. Pero si Python realmente solo permite un solo hilo en cualquier momento (time slicing threading), entonces no debería ser posible que exista un valor obsoleto, ¿no?