Sind Listen threadsicher?

154

Ich stelle fest, dass häufig empfohlen wird, Warteschlangen mit mehreren Threads anstelle von Listen und zu verwenden .pop(). Liegt das daran, dass Listen nicht threadsicher sind oder aus einem anderen Grund?

lemiant
quelle
1
Es ist schwer immer zu sagen, was genau in Python als threadsicher garantiert ist, und es ist schwer zu überlegen, ob Threadsicherheit darin vorhanden ist. Sogar die sehr beliebte Bitcoin-Brieftasche Electrum hatte Parallelitätsfehler, die wahrscheinlich darauf zurückzuführen sind.
Sudo

Antworten:

181

Listen selbst sind threadsicher. In CPython schützt die GIL vor gleichzeitigen Zugriffen auf sie, und andere Implementierungen achten darauf, für ihre Listenimplementierungen eine fein abgestimmte Sperre oder einen synchronisierten Datentyp zu verwenden. Obwohl Listen selbst nicht durch Versuche, gleichzeitig darauf zuzugreifen, beschädigt werden können, sind die Daten der Listen nicht geschützt. Beispielsweise:

L[0] += 1

Es wird nicht garantiert, dass L [0] tatsächlich um eins erhöht wird, wenn ein anderer Thread dasselbe tut, da +=es sich nicht um eine atomare Operation handelt. (Sehr, sehr wenige Operationen in Python sind tatsächlich atomar, da die meisten dazu führen können, dass beliebiger Python-Code aufgerufen wird.) Sie sollten Warteschlangen verwenden, da Sie aufgrund der Rasse möglicherweise das falsche Element erhalten oder löschen, wenn Sie nur eine ungeschützte Liste verwenden Bedingungen.

Thomas Wouters
quelle
1
Ist Deque auch fadensicher? Es scheint für meinen Gebrauch angemessener zu sein.
Lemiant
20
Alle Python-Objekte haben die gleiche Art von Thread-Sicherheit - sie selbst werden nicht beschädigt, aber ihre Daten können. collection.deque steht hinter Queue.Queue-Objekten. Wenn Sie über zwei Threads auf Dinge zugreifen, sollten Sie wirklich Queue.Queue-Objekte verwenden. Ja wirklich.
Thomas Wouters
10
lemiant, deque ist fadensicher. Aus Kapitel 2 von Fluent Python: "Die Klasse collection.deque ist eine thread-sichere Warteschlange mit zwei Enden, die zum schnellen Einfügen und Entfernen an beiden Enden entwickelt wurde. [...] Die Anhänge- und Popleft-Operationen sind atomar, daher ist deque sicher Verwendung als LIFO-Warteschlange in Multithread-Anwendungen, ohne dass Sperren verwendet werden müssen. "
Al Sweigart
3
Geht es bei dieser Antwort um CPython oder um Python? Was ist die Antwort für Python selbst?
user541686
@Nils: äh, die erste Seite , die Sie verknüpfen sagt Python statt CPython , weil es ist die Sprache Python beschreiben. Und dieser zweite Link besagt buchstäblich, dass es mehrere Implementierungen der Python-Sprache gibt, von denen nur eine populärer ist. Angesichts der Frage zu Python sollte in der Antwort beschrieben werden, was bei jeder konformen Implementierung von Python garantiert werden kann, nicht nur, was insbesondere in CPython geschieht.
user541686
89

Um einen Punkt in Thomas' ausgezeichneten Antwort zu klären, soll erwähnt werden , dass append() ist Thread - sicher.

Dies liegt daran, dass keine Bedenken bestehen, dass sich die gelesenen Daten an derselben Stelle befinden, sobald wir sie schreiben . Die append()Operation liest keine Daten, sondern schreibt nur Daten in die Liste.

dotancohen
quelle
1
PyList_Append liest aus dem Speicher. Meinen Sie damit, dass die Lese- und Schreibvorgänge in derselben GIL-Sperre stattfinden? github.com/python/cpython/blob/…
amwinter
1
@amwinter Ja, der gesamte Aufruf von PyList_Appenderfolgt in einer GIL-Sperre. Es wird ein Verweis auf ein Objekt zum Anhängen gegeben. Der Inhalt dieses Objekts kann nach der Auswertung und vor dem Aufruf von geändert werden PyList_Append. Aber es wird immer noch dasselbe Objekt sein und sicher angehängt (wenn Sie dies tun lst.append(x); ok = lst[-1] is x, okkann es natürlich falsch sein). Der Code, auf den Sie verweisen, liest nicht aus dem angehängten Objekt, außer um es zu ERHÖHEN. Es liest die angehängte Liste und kann sie neu zuordnen.
Greggo
2
dotancohen ‚s Punkt ist , dass L[0] += xein durchführen wird __getitem__auf Lund dann __setitem__auf L- wenn Lunterstützt __iadd__es die Dinge ein wenig tun wird anders im Objekt - Schnittstelle, aber es gibt immer noch zwei getrennte Operationen Lan der Python - Interpreter Ebene (Sie werden sie in dem sehen kompilierter Bytecode). Das appendist in aa einzigen Methodenaufruf im Bytecode getan.
Greggo
6
Wie wäre es remove?
Acrazing
2
upvoted! Kann ich also kontinuierlich einen Thread anhängen und einen anderen Thread einfügen?
PirateApp
3

Ich hatte kürzlich diesen Fall, in dem ich eine Liste kontinuierlich in einem Thread anhängen, die Elemente durchlaufen und prüfen musste, ob das Element bereit war. In meinem Fall war es ein AsyncResult und es nur dann aus der Liste entfernen, wenn es bereit war. Ich konnte keine Beispiele finden, die mein Problem klar demonstrierten. Hier ist ein Beispiel, das das kontinuierliche Hinzufügen zur Liste in einem Thread und das kontinuierliche Entfernen aus derselben Liste in einem anderen Thread demonstriert. Die fehlerhafte Version läuft problemlos mit kleineren Zahlen, aber halten Sie die Zahlen groß genug und führen Sie a aus einige Male und Sie werden den Fehler sehen

Die FLAWED-Version

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)

Ausgabe bei FEHLER

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

Version, die Sperren verwendet

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)

Ausgabe

[] # Empty list

Fazit

Wie in den früheren Antworten erwähnt, ist das Anhängen oder Löschen von Elementen aus der Liste selbst threadsicher. Was nicht threadsicher ist, ist, wenn Sie einen Thread anhängen und einen anderen einfügen

PirateApp
quelle
5
Die Version mit Sperren hat das gleiche Verhalten wie die ohne Sperren. Grundsätzlich tritt der Fehler auf, weil versucht wird, etwas zu entfernen, das nicht in der Liste enthalten ist. Dies hat nichts mit der Thread-Sicherheit zu tun. Versuchen Sie, die Version mit Sperren auszuführen, nachdem Sie die Startreihenfolge geändert haben, dh starten Sie t2 vor t1, und Sie werden denselben Fehler sehen. Immer wenn t2 vor t1 steht, tritt der Fehler auf, unabhängig davon, ob Sie Sperren verwenden oder nicht.
Dev
1
Außerdem ist es besser, einen Kontextmanager ( with r:) zu verwenden, als explizit aufzurufen r.acquire()undr.release()
GordonAitchJay
1
@ GordonAitchJay
Timothy