¿Las listas son seguras para subprocesos?


155

Noto que a menudo se sugiere usar colas con múltiples hilos, en lugar de listas y .pop(). ¿Es esto porque las listas no son seguras para subprocesos o por alguna otra razón?


1
Es difícil decir siempre qué es exactamente seguro para subprocesos en Python, y es difícil razonar sobre la seguridad de subprocesos en él. Incluso la muy popular billetera Bitcoin Electrum ha tenido errores de concurrencia probablemente derivados de esto.
sudo

Respuestas:


182

Las listas en sí son seguras para subprocesos. En CPython, el GIL protege contra accesos concurrentes a ellos, y otras implementaciones se encargan de usar un bloqueo de grano fino o un tipo de datos sincronizado para sus implementaciones de listas. Sin embargo, si bien las listas en no pueden corromperse por intentos de acceso simultáneo, los datos de las listas no están protegidos. Por ejemplo:

L[0] += 1

no se garantiza que aumente L [0] en uno si otro hilo hace lo mismo, porque +=no es una operación atómica. (Muy, muy pocas operaciones en Python son realmente atómicas, porque la mayoría de ellas pueden provocar que se invoque un código arbitrario de Python). Debería usar Colas porque si solo usa una lista desprotegida, puede obtener o eliminar el elemento incorrecto debido a la raza condiciones


1
¿Deque también es seguro para subprocesos? Parece más apropiado para mi uso.
lemiant

20
Todos los objetos de Python tienen el mismo tipo de seguridad de subprocesos: ellos mismos no se corrompen, pero sus datos pueden. collections.deque es lo que hay detrás de los objetos Queue.Queue. Si está accediendo a cosas desde dos hilos, realmente debería usar objetos Queue.Queue. De Verdad.
Thomas Wouters

10
lemiant, deque es a prueba de hilos. Del Capítulo 2 de Fluent Python: "La clase collections.deque es una cola de doble extremo segura para subprocesos diseñada para una rápida inserción y extracción de ambos extremos. [...] Las operaciones de anexar y popleft son atómicas, por lo que deque es seguro para usar como una cola LIFO en aplicaciones de subprocesos múltiples sin la necesidad de usar bloqueos ".
Al Sweigart

3
¿Es esta respuesta sobre CPython o sobre Python? ¿Cuál es la respuesta para Python en sí?
user541686

@Nils: Uh, la primera página se ha vinculado a dice Python en lugar de CPython porque está describiendo el lenguaje Python. Y ese segundo enlace literalmente dice que hay múltiples implementaciones del lenguaje Python, solo una que resulta ser más popular. Dado que la pregunta era sobre Python, la respuesta debería describir qué se puede garantizar que suceda en cualquier implementación conforme de Python, no solo lo que sucede en CPython en particular.
user541686

89

Para aclarar un punto en la excelente respuesta de Thomas, debe mencionarse que append() es seguro para subprocesos.

Esto se debe a que no hay preocupación de que los datos que se leen estarán en el mismo lugar una vez que lo escribamos . La append()operación no lee datos, solo escribe datos en la lista.


1
PyList_Append está leyendo de la memoria. ¿Quiere decir que sus lecturas y escrituras ocurren en el mismo bloqueo GIL? github.com/python/cpython/blob/…
amwinter

1
@amwinter Sí, toda la llamada a PyList_Appendse realiza en un bloqueo GIL. Se le da una referencia a un objeto para agregar. El contenido de ese objeto puede cambiarse después de que se evalúa y antes de que PyList_Appendse realice la llamada . Pero seguirá siendo el mismo objeto y se agregará de forma segura (si lo hace lst.append(x); ok = lst[-1] is x, okpuede ser falso, por supuesto). El código al que hace referencia no lee del objeto adjunto, excepto para INCREMENTARLO. Lee y puede reasignar la lista a la que se agrega.
greggo

3
El punto de dotancohen es que L[0] += xrealizará un __getitem__encendido Ly luego un __setitem__encendido L; si es Lcompatible __iadd__, hará las cosas un poco diferente en la interfaz del objeto, pero todavía hay dos operaciones separadas Len el nivel de intérprete de Python (las verá en el bytecode compilado). El appendse realiza en una llamada de método único en el código de bytes.
greggo

66
¿Qué tal remove?
acrazing

2
¡Votado! entonces, ¿puedo agregar un hilo de forma continua y agregar otro hilo?
PirateApp


2

Recientemente tuve este caso en el que necesitaba agregar una lista continuamente en un hilo, recorrer los elementos y verificar si el elemento estaba listo, en mi caso era un AsyncResult y eliminarlo de la lista solo si estaba listo. No pude encontrar ningún ejemplo que demostrara mi problema claramente. Aquí hay un ejemplo que demuestra agregar a la lista en un hilo continuamente y eliminar de la misma lista en otro hilo continuamente. unas pocas veces y verás el error

La versión defectuosa

import threading
import time

# Change this number as you please, bigger numbers will get the error quickly
count = 1000
l = []

def add():
    for i in range(count):
        l.append(i)
        time.sleep(0.0001)

def remove():
    for i in range(count):
        l.remove(i)
        time.sleep(0.0001)


t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()

print(l)

Salida cuando ERROR

Exception in thread Thread-63:
Traceback (most recent call last):
  File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "<ipython-input-30-ecfbac1c776f>", line 13, in remove
    l.remove(i)
ValueError: list.remove(x): x not in list

Versión que usa cerraduras

import threading
import time
count = 1000
l = []
lock = threading.RLock()
def add():
    with lock:
        for i in range(count):
            l.append(i)
            time.sleep(0.0001)

def remove():
    with lock:
        for i in range(count):
            l.remove(i)
            time.sleep(0.0001)


t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()

print(l)

Salida

[] # Empty list

Conclusión

Como se mencionó en las respuestas anteriores, mientras que el acto de agregar o extraer elementos de la lista es seguro para subprocesos, lo que no es seguro para subprocesos es cuando se agrega en un subproceso y aparece en otro


66
La versión con bloqueos tiene el mismo comportamiento que la que no tiene bloqueos. Básicamente, el error viene porque está tratando de eliminar algo que no está en la lista, no tiene nada que ver con la seguridad del hilo. Intente ejecutar la versión con bloqueos después de cambiar el orden de inicio, es decir, inicie t2 antes de t1 y verá el mismo error. cada vez que t2 se adelanta a t1, el error ocurrirá sin importar si usa bloqueos o no.
Dev

1
Además, es mejor usar un administrador de contexto ( with r:) en lugar de llamar explícitamente r.acquire()yr.release()
GordonAitchJay

1
@GordonAitchJay 👍
Timothy C. Quinn
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.