¿Son innecesarios los lockings en el código Python de subprocesos múltiples debido a la GIL?

Si está confiando en una implementación de Python que tiene un locking global de intérprete (es decir, CPython) y está escribiendo código de multiproceso, ¿realmente necesita lockings?

Si la 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 acerca de Python en máquinas con varios procesadores / núcleo.

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

Aún necesitarás lockings si compartes el estado entre los hilos. El GIL solo protege al intérprete internamente. Aún puedes tener actualizaciones inconsistentes en tu 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 puede interrumpirse entre la lectura del estado compartido ( balance = shared_balance ) y la escritura del resultado modificado ( shared_balance = balance ), causando una actualización perdida. El resultado es un valor aleatorio para el estado compartido.

Para que las actualizaciones sean coherentes, los métodos de ejecución necesitarían bloquear el estado compartido en torno a 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ó .

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

La esencia del locking es asegurar que un bloque de código en particular solo sea ejecutado por un hilo. La GIL aplica esto para bloques del tamaño de un solo bytecode, pero generalmente desea que el locking abarque un bloque de código más grande que este.

Agregando a la discusión:

Debido a que GIL existe, algunas operaciones son atómicas en Python y no necesitan un locking.

http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe

Sin embargo, como indican las otras respuestas, aún debe usar lockings siempre que la lógica de la aplicación los requiera (como en un problema del Productor / Consumidor).

El locking global del intérprete evita que los hilos accedan al intérprete simultáneamente (por lo tanto, CPython solo usa un núcleo). Sin embargo, como lo entiendo, los subprocesos todavía se interrumpen y se progtwign de forma preventiva , lo que significa que todavía necesitas lockings en las estructuras de datos compartidas, para que tus subprocesos no toquen los dedos de los pies.

La respuesta que he encontrado una y otra vez es que el multithreading en Python rara vez vale la pena, debido a esto. He escuchado cosas buenas sobre el proyecto PyProcessing , que hace que la ejecución de múltiples procesos sea tan “simple” como multiproceso, con estructuras de datos compartidas, colas, etc. (PyProcessing se introducirá en la biblioteca estándar del próximo Python 2.6 como módulo de multiprocesamiento .) Esto te lleva a la 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 valor predeterminado se puede cambiar), el núcleo libera la GIL para el hilo actual. En ese momento, el sistema operativo elige un subproceso de todos los subprocesos que compiten por el locking (posiblemente eligiendo el mismo subproceso que acaba de lanzar la GIL; no tiene ningún control sobre qué subproceso 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 puro de Python. Se pueden escribir extensiones (bibliotecas externas de Python escritas normalmente en C) que liberan el locking, 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 locking.

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

Piénsalo de esta manera:

En una computadora con un solo procesador, el subprocesamiento múltiple ocurre al suspender un hilo e iniciar otro lo suficientemente rápido para 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 de este tipo:

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

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

 b = 5 

Luego, cuando se reanuda la otra hebra, b se sobrescribe con los valores calculados antiguos, que probablemente no es lo que se esperaba.

Así que puedes ver que a pesar de que en realidad no se están ejecutando al mismo tiempo, aún necesitas el locking.

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

Todavía se necesitan cerraduras. Intentaré explicar por qué son necesarios.

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 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.

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

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

Como se indicó anteriormente, GIL solo se asegura de que dos subprocesos no puedan ejecutar una instrucción simultáneamente, lo que significa que ambos subprocesos no pueden ejecutar read_var = var en un momento determinado. Pero pueden ejecutar la instrucción una tras otra y aún puede tener problemas. Considera esta situación:

  • Supongamos que read_var es 0.
  • GIL se mantiene por 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 ejecutará para ningún otro hilo en este momento.
  • GIL se 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 debería convertirse en 2.
  • Por lo tanto, se debe utilizar un locking para mantener tanto la lectura como el incremento como una operación atómica.
  • La respuesta de Will 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 statement de verificación de valor en el retiro y ya no veo 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 locking? (Suponiendo que solo hablamos de código de python puro)

Si entiendo correctamente, la verificación de condición anterior no funcionaría en un entorno de subprocesos real . Cuando se ejecutan simultáneamente más de un subproceso, se puede crear un valor obsoleto, por lo tanto, la inconsistencia del estado de compartir, entonces realmente necesita un locking. Pero si Python realmente solo permite un solo hilo en cualquier momento (tiempo de corte de hilos), entonces no debería ser posible que exista un valor antiguo, ¿verdad?