Sind Sperren in Python-Code mit mehreren Threads aufgrund der GIL nicht erforderlich?

76

Wenn Sie sich auf eine Implementierung von Python verlassen, die über eine globale Interpreter-Sperre (dh CPython) verfügt und Multithread-Code schreibt, benötigen Sie überhaupt Sperren?

Wenn die GIL nicht zulässt, dass mehrere Anweisungen gleichzeitig ausgeführt werden, sind gemeinsame Daten dann nicht zum Schutz unnötig?

Es tut mir leid, wenn dies eine dumme Frage ist, aber ich habe mich immer über Python auf Multiprozessor- / Core-Computern gewundert.

Das Gleiche gilt für jede andere Sprachimplementierung mit einer GIL.

Corey Goldberg
quelle
1
Beachten Sie auch, dass die GIL und Implementierungsdetails sind. IronPython und Jython haben beispielsweise keine GIL.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

Antworten:

72

Sie benötigen weiterhin Sperren, wenn Sie den Status zwischen Threads teilen. Die GIL schützt den Dolmetscher nur intern. Sie können immer noch inkonsistente Aktualisierungen in Ihrem eigenen Code haben.

Zum Beispiel:

#!/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

Hier kann Ihr Code zwischen dem Lesen des freigegebenen Status ( balance = shared_balance) und dem Zurückschreiben des geänderten Ergebnisses ( shared_balance = balance) unterbrochen werden , was zu einem verlorenen Update führt. Das Ergebnis ist ein zufälliger Wert für den gemeinsam genutzten Status.

Um die Aktualisierungen konsistent zu machen, müssten Ausführungsmethoden den freigegebenen Status um die Lese-, Änderungs- und Schreibabschnitte (innerhalb der Schleifen) sperren oder auf irgendeine Weise erkennen, wann sich der freigegebene Status seit dem Lesen geändert hat .

Will Harris
quelle
Das Codebeispiel gibt ein klares und visuelles Verständnis! Netter Beitrag Harris! Ich wünschte, ich könnte zweimal abstimmen!
RayLuo
Wird es sicher sein, wenn es nur eine Zeile gibt shared_balance += 100und shared_balance -= 100?
Mrgloom
24

Nein - die GIL schützt Python-Interna nur vor mehreren Threads, die ihren Status ändern. Dies ist eine sehr niedrige Sperrstufe, die nur ausreicht, um die eigenen Strukturen von Python in einem konsistenten Zustand zu halten. Es behandelt nicht die Sperre auf Anwendungsebene , die Sie durchführen müssen, um die Thread-Sicherheit in Ihrem eigenen Code zu behandeln.

Das Wesentliche beim Sperren besteht darin, sicherzustellen, dass ein bestimmter Codeblock nur von einem Thread ausgeführt wird. Die GIL erzwingt dies für Blöcke mit der Größe eines einzelnen Bytecodes. Normalerweise soll die Sperre jedoch einen größeren Codeblock umfassen.

Brian
quelle
9

Dieser Beitrag beschreibt die GIL auf einem ziemlich hohen Niveau:

Von besonderem Interesse sind diese Zitate:

Alle zehn Anweisungen (diese Standardeinstellung kann geändert werden) gibt der Kern die GIL für den aktuellen Thread frei. Zu diesem Zeitpunkt wählt das Betriebssystem einen Thread aus allen Threads aus, die um die Sperre konkurrieren (möglicherweise wird derselbe Thread ausgewählt, der gerade die GIL freigegeben hat - Sie haben keine Kontrolle darüber, welcher Thread ausgewählt wird). Dieser Thread erfasst die GIL und wird dann für weitere zehn Bytecodes ausgeführt.

und

Beachten Sie sorgfältig, dass die GIL nur reinen Python-Code einschränkt. Es können Erweiterungen (externe Python-Bibliotheken, die normalerweise in C geschrieben sind) geschrieben werden, die die Sperre aufheben. Dadurch kann der Python-Interpreter getrennt von der Erweiterung ausgeführt werden, bis die Erweiterung die Sperre wiedererlangt.

Es hört sich so an, als ob die GIL nur weniger mögliche Instanzen für einen Kontextwechsel bereitstellt und Multi-Core- / Prozessorsysteme in Bezug auf jede Python-Interpreter-Instanz als einen einzigen Kern verhalten. Ja, Sie müssen also immer noch Synchronisationsmechanismen verwenden.

rcreswick
quelle
2
Hinweis: sys.getcheckinterval()Gibt an, wie viele Bytecode-Anweisungen zwischen "GIL-Releases" ausgeführt werden (und seit mindestens 2,5 sind es 100 (nicht 10)). In 3.2 wird möglicherweise auf ein zeitbasiertes Intervall (ca. 5 ms) umgeschaltet, anstatt auf Befehlszählungen. Die Änderung kann auch auf 2.7 angewendet werden, obwohl noch in Arbeit ist.
Peter Hansen
8

Die globale Interpreter-Sperre verhindert, dass Threads gleichzeitig auf den Interpreter zugreifen (daher verwendet CPython immer nur einen Kern). Soweit ich weiß, werden die Threads jedoch immer noch unterbrochen und präventiv geplant. Dies bedeutet, dass Sie weiterhin Sperren für gemeinsam genutzte Datenstrukturen benötigen, damit Ihre Threads nicht gegenseitig auf die Zehen treten.

Die Antwort, auf die ich immer wieder gestoßen bin, ist, dass Multithreading in Python aus diesem Grund den Aufwand selten wert ist. Ich habe gute Dinge über das PyProcessing- Projekt gehört, das das Ausführen mehrerer Prozesse so einfach wie Multithreading mit gemeinsam genutzten Datenstrukturen, Warteschlangen usw. macht. (PyProcessing wird als Multiprocessing- Modul in die Standardbibliothek des kommenden Python 2.6 eingeführt .) Dies führt Sie um die GIL herum, da jeder Prozess seinen eigenen Interpreter hat.

David Eyk
quelle
4

Denk darüber so:

Auf einem Computer mit einem Prozessor erfolgt Multithreading, indem ein Thread angehalten und ein anderer schnell genug gestartet wird, damit er zur gleichen Zeit ausgeführt wird. Dies ist wie Python mit der GIL: Es wird immer nur ein Thread ausgeführt.

Das Problem ist, dass der Thread überall angehalten werden kann, wenn ich beispielsweise b = (a + b) * 3 berechnen möchte, kann dies zu folgenden Anweisungen führen:

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

Nehmen wir nun an, dass der Thread in einem Thread ausgeführt wird und dieser Thread nach Zeile 1 oder 2 angehalten wird und dann ein anderer Thread aktiviert wird:

b = 5

Wenn der andere Thread fortgesetzt wird, wird b durch die alten berechneten Werte überschrieben, was wahrscheinlich nicht den Erwartungen entspricht.

Sie können also sehen, dass Sie, obwohl sie nicht WIRKLICH gleichzeitig ausgeführt werden, dennoch gesperrt werden müssen.


quelle
1

Sie müssen weiterhin Sperren verwenden (Ihr Code kann jederzeit unterbrochen werden, um einen anderen Thread auszuführen, und dies kann zu Dateninkonsistenzen führen). Das Problem mit GIL besteht darin, dass verhindert wird, dass Python-Code mehr Kerne gleichzeitig verwendet (oder mehrere Prozessoren, falls verfügbar).

rslite
quelle
1

Schlösser werden noch benötigt. Ich werde versuchen zu erklären, warum sie gebraucht werden.

Jede Operation / Anweisung wird im Interpreter ausgeführt. GIL stellt sicher, dass der Interpreter zu einem bestimmten Zeitpunkt von einem einzelnen Thread gehalten wird . Und Ihr Programm mit mehreren Threads funktioniert in einem einzigen Interpreter. Zu einem bestimmten Zeitpunkt wird dieser Interpreter von einem einzelnen Thread gehalten. Es bedeutet , dass nur Threads, die Interpreter Halten läuft zu jedem Zeitpunkt der Zeit.

Angenommen, es gibt zwei Threads, z. B. t1 und t2, und beide möchten zwei Anweisungen ausführen, die den Wert einer globalen Variablen lesen und inkrementieren.

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

Wie oben beschrieben, stellt GIL nur sicher, dass zwei Threads einen Befehl nicht gleichzeitig ausführen können, was bedeutet, dass beide Threads read_var = varzu einem bestimmten Zeitpunkt nicht ausgeführt werden können. Aber sie können Anweisungen nacheinander ausführen, und Sie können immer noch Probleme haben. Betrachten Sie diese Situation:

  • Angenommen, read_var ist 0.
  • GIL wird vom Gewinde t1 gehalten.
  • t1 wird ausgeführt read_var = var. Read_var in t1 ist also 0. GIL stellt nur sicher, dass diese Leseoperation zu diesem Zeitpunkt für keinen anderen Thread ausgeführt wird.
  • GIL wird dem Thread t2 gegeben.
  • t2 wird ausgeführt read_var = var. Aber read_var ist immer noch 0. Also ist read_var in t2 0.
  • GIL wird zu t1 gegeben.
  • t1 wird ausgeführt var = read_var+1und var wird 1.
  • GIL wird zu t2 gegeben.
  • t2 denkt read_var = 0, weil es das ist, was es liest.
  • t2 wird ausgeführt var = read_var+1und var wird 1.
  • Unsere Erwartung war, dass var2 werden sollte.
  • Daher muss eine Sperre verwendet werden, um das Lesen und Inkrementieren als atomare Operation aufrechtzuerhalten.
  • Will Harris 'Antwort erklärt es anhand eines Codebeispiels.
Akshar Raaj
quelle
0

Ein kleines Update aus Will Harris 'Beispiel:

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

Geben Sie eine Wertprüfungserklärung in die Auszahlung ein und ich sehe kein Negativ mehr und die Aktualisierungen scheinen konsistent zu sein. Meine Frage ist:

Wenn GIL verhindert, dass zu einem atomaren Zeitpunkt nur ein Thread ausgeführt werden kann, wo wäre dann der veraltete Wert? Wenn kein veralteter Wert, warum brauchen wir eine Sperre? (Angenommen, wir sprechen nur über reinen Python-Code)

Wenn ich das richtig verstehe, würde die obige Bedingungsprüfung in einer echten Threading-Umgebung nicht funktionieren . Wenn mehr als ein Thread gleichzeitig ausgeführt wird, kann ein veralteter Wert erstellt werden, daher die Inkonsistenz des Freigabestatus. Dann benötigen Sie wirklich eine Sperre. Aber wenn Python wirklich immer nur einen Thread zulässt (Time Slicing Threading), sollte es nicht möglich sein, dass veralteter Wert existiert, oder?

Jimx
quelle
Ok sieht so aus, als würde GIL den Thread nicht die ganze Zeit sperren und es könnte immer noch zu einem Kontextwechsel kommen. Also ich irre mich, Schloss wird noch benötigt.
Jimx