Le liste sono thread-safe?


155

Ho notato che viene spesso suggerito di utilizzare le code con più thread, anziché elenchi e .pop(). Questo perché gli elenchi non sono thread-safe o per qualche altro motivo?


1
Difficile dire sempre cosa sia esattamente sicuro per i thread in Python, ed è difficile ragionare sulla sicurezza dei thread in esso. Anche il famoso portafoglio Bitcoin Electrum ha avuto probabilmente problemi di concorrenza derivanti da questo.
sudo,

Risposte:


182

Le liste stesse sono thread-safe. In CPython il GIL protegge da accessi simultanei ad essi e altre implementazioni si occupano di utilizzare un blocco a grana fine o un tipo di dati sincronizzato per le loro implementazioni di elenchi. Tuttavia, mentre gli elenchi stessi non possono essere danneggiati dai tentativi di accesso simultaneo, i dati degli elenchi non sono protetti. Per esempio:

L[0] += 1

non è garantito che aumenti effettivamente L [0] di uno se un altro thread fa la stessa cosa, perché +=non è un'operazione atomica. (Pochissime operazioni in Python sono in realtà atomiche, poiché la maggior parte di esse può causare la chiamata di codice Python arbitrario.) Dovresti usare le code perché se usi solo un elenco non protetto, potresti ottenere o eliminare l'elemento sbagliato a causa della razza condizioni.


1
Deque è anche sicuro per i thread? Sembra più appropriato per il mio uso.
lemiant

20
Tutti gli oggetti Python hanno lo stesso tipo di sicurezza del thread: essi stessi non vanno danneggiati, ma i loro dati possono. collections.deque è ciò che sta dietro gli oggetti Queue.Queue. Se accedi a cose da due thread, dovresti davvero usare gli oggetti Queue.Queue. Veramente.
Thomas Wouters,

10
lemiant, deque è thread-safe. Dal capitolo 2 di Fluent Python: "La classe collections.deque è una coda a doppia estremità protetta da thread progettata per l'inserimento e la rimozione rapidi da entrambe le estremità. [...] Le operazioni di append e popleft sono atomiche, quindi il deque è sicuro da utilizzare come coda LIFO in applicazioni multi-thread senza la necessità di utilizzare i blocchi. "
Al Sweigart,

3
Questa risposta riguarda CPython o Python? Qual è la risposta per Python stesso?
user541686,

@Nils: Uh, la prima pagina si è collegato al dice Python, invece di CPython perché si descrive il linguaggio Python. E quel secondo link dice letteralmente che ci sono molteplici implementazioni del linguaggio Python, solo uno che sembra essere più popolare. Dato che la domanda riguardava Python, la risposta dovrebbe descrivere cosa può essere garantito che accada in qualsiasi implementazione conforme di Python, non solo ciò che accade in CPython in particolare.
user541686,

90

Per chiarire un punto dell'eccellente risposta di Thomas, è necessario ricordare che append() è sicuro per i thread.

Questo perché non vi è alcuna preoccupazione che i dati letti saranno nello stesso posto una volta che andremo a scriverli . L' append()operazione non legge i dati, scrive solo i dati nell'elenco.


1
PyList_Append sta leggendo dalla memoria. Vuoi dire che le sue letture e scritture avvengono nello stesso blocco GIL? github.com/python/cpython/blob/…
amwinter

1
@amwinter Sì, l'intera chiamata a PyList_Appendviene eseguita in un blocco GIL. Viene fornito un riferimento a un oggetto da aggiungere. Il contenuto di quell'oggetto potrebbe essere modificato dopo che è stato valutato e prima che PyList_Appendvenga effettuata la chiamata . Ma sarà comunque lo stesso oggetto e aggiunto in modo sicuro (se lo fai lst.append(x); ok = lst[-1] is x, allora okpotrebbe essere Falso, ovviamente). Il codice a cui fai riferimento non legge dall'oggetto allegato, tranne per aumentarlo. Legge e può riallocare l'elenco a cui è stato aggiunto.
Greggo,

3
Il punto di dotancohen è che L[0] += xeseguirà un __getitem__on Le poi un __setitem__on L- se Lsupportato __iadd__farà le cose in modo un po 'diverso nell'interfaccia dell'oggetto, ma ci sono ancora due operazioni separate La livello di interprete python (le vedrai nel compilato bytecode). Il appendè fatto in aa metodo singola chiamata nel bytecode.
Greggo,

6
Che ne dici remove?
Inaugurazione

2
upvoted! quindi posso aggiungere continuamente un thread e inserire un altro thread?
PirateApp


2

Recentemente ho avuto questo caso in cui dovevo aggiungere continuamente un elenco in un thread, scorrere gli elementi e controllare se l'articolo era pronto, era un AsyncResult nel mio caso e rimuoverlo dall'elenco solo se era pronto. Non sono riuscito a trovare alcun esempio che dimostrasse chiaramente il mio problema Ecco un esempio che dimostra l'aggiunta continua all'elenco in un thread e la rimozione continua dallo stesso elenco in un altro thread La versione difettosa viene eseguita facilmente su numeri più piccoli ma mantiene i numeri abbastanza grandi ed esegue un alcune volte e vedrai l'errore

La versione FLAWED

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)

Uscita quando ERRORE

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

Versione che utilizza i blocchi

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)

Produzione

[] # Empty list

Conclusione

Come menzionato nelle risposte precedenti mentre l'atto di aggiungere o estrarre elementi dall'elenco stesso è thread-safe, ciò che non è thread-safe è quando si aggiunge un thread e si apre un altro


6
La versione con blocchi ha lo stesso comportamento di quella senza blocchi. Fondamentalmente l'errore sta arrivando perché sta cercando di rimuovere qualcosa che non è nell'elenco, non ha nulla a che fare con la sicurezza del thread. Prova a eseguire la versione con i blocchi dopo aver modificato l'ordine di avvio, ad esempio avvia t2 prima di t1 e vedrai lo stesso errore. ogni volta che t2 precede t1, l'errore si verificherà indipendentemente dal fatto che si utilizzino i blocchi o meno.
Dev

1
Inoltre, stai meglio usando un gestore di contesto ( with r:) invece di chiamare esplicitamente r.acquire()er.release()
GordonAitchJay

1
@GordonAitchJay 👍
Timothy C. Quinn,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.