Was ist die Python Global Interpreter Lock (GIL)?

Was ist die Python Global Interpreter Lock (GIL)?

Die Python Global Interpreter-Sperre oderGIL ist in einfachen Worten ein Mutex (oder eine Sperre), mit dem nur ein Thread die Kontrolle über den Python-Interpreter übernehmen kann.

Dies bedeutet, dass zu jedem Zeitpunkt nur ein Thread ausgeführt werden kann. Die Auswirkungen der GIL sind für Entwickler, die Single-Thread-Programme ausführen, nicht sichtbar, können jedoch einen Leistungsengpass bei CPU-gebundenem und Multi-Thread-Code darstellen.

Da mit der GIL auch in einer Multithread-Architektur mit mehr als einem CPU-Kern jeweils nur ein Thread ausgeführt werden kann, hat sich die GIL einen Ruf als „berüchtigtes“ Feature von Python erworben.

In diesem Artikel erfahren Sie, wie sich die GIL auf die Leistung Ihrer Python-Programme auswirkt und wie Sie die Auswirkungen auf Ihren Code verringern können.

Welches Problem hat die GIL für Python gelöst?

Python verwendet die Referenzzählung für die Speicherverwaltung. Dies bedeutet, dass in Python erstellte Objekte eine Referenzzählvariable haben, die die Anzahl der Referenzen verfolgt, die auf das Objekt verweisen. Wenn dieser Zähler Null erreicht, wird der vom Objekt belegte Speicher freigegeben.

Schauen wir uns ein kurzes Codebeispiel an, um zu demonstrieren, wie die Referenzzählung funktioniert:

>>>

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

Im obigen Beispiel betrug der Referenzzähler für das leere Listenobjekt[] 3. Das Listenobjekt wurde vona,b referenziert und das Argument ansys.getrefcount() übergeben.

Zurück zur GIL:

Das Problem bestand darin, dass diese Referenzzählvariable vor Rennbedingungen geschützt werden musste, bei denen zwei Threads gleichzeitig ihren Wert erhöhen oder verringern. In diesem Fall kann entweder ein Speicherverlust auftreten, der niemals freigegeben wird, oder, noch schlimmer, der Speicher wird fälschlicherweise freigegeben, solange noch ein Verweis auf dieses Objekt vorhanden ist. Dies kann zu Abstürzen oder anderen „seltsamen“ Fehlern in Ihren Python-Programmen führen.

Diese Referenzzählvariable kann sicher gehalten werden, indemlocks zu allen Datenstrukturen hinzugefügt werden, die für mehrere Threads gemeinsam genutzt werden, damit sie nicht inkonsistent geändert werden.

Das Hinzufügen einer Sperre zu jedem Objekt oder jeder Gruppe von Objekten bedeutet jedoch, dass mehrere Sperren vorhanden sind, die ein anderes Problem verursachen können - Deadlocks (Deadlocks können nur auftreten, wenn mehr als eine Sperre vorhanden ist). Ein weiterer Nebeneffekt wäre eine Leistungsminderung, die durch die wiederholte Erfassung und Freigabe von Sperren verursacht wird.

Die GIL ist eine einzelne Sperre für den Interpreter selbst, die eine Regel hinzufügt, dass für die Ausführung eines Python-Bytecodes die Interpreter-Sperre erforderlich ist. Dies verhindert Deadlocks (da es nur eine Sperre gibt) und führt nicht zu einem hohen Leistungsaufwand. Aber es macht effektiv jedes CPU-gebundene Python-Programm Single-Threaded.

Die GIL wird zwar von Dolmetschern für andere Sprachen wie Ruby verwendet, ist jedoch nicht die einzige Lösung für dieses Problem. Einige Sprachen vermeiden die Anforderung einer GIL für die threadsichere Speicherverwaltung, indem sie andere Ansätze als die Referenzzählung verwenden, z. B. die Speicherbereinigung.

Auf der anderen Seite bedeutet dies, dass diese Sprachen häufig den Verlust von Single-Threaded-Leistungsvorteilen einer GIL durch Hinzufügen anderer leistungssteigernder Funktionen wie JIT-Compiler kompensieren müssen.

Warum wurde die GIL als Lösung gewählt?

Warum wurde in Python ein scheinbar so hinderlicher Ansatz verwendet? War es eine schlechte Entscheidung der Entwickler von Python?

Nun, inwords of Larry Hastings ist die Designentscheidung der GIL eines der Dinge, die Python so populär gemacht haben wie heute.

Python gibt es schon seit den Tagen, als Betriebssysteme kein Thread-Konzept hatten. Python wurde so konzipiert, dass es einfach zu bedienen ist, um die Entwicklung zu beschleunigen und immer mehr Entwickler damit zu beginnen.

Für die vorhandenen C-Bibliotheken, deren Funktionen in Python benötigt wurden, wurden viele Erweiterungen geschrieben. Um inkonsistente Änderungen zu vermeiden, erforderten diese C-Erweiterungen eine thread-sichere Speicherverwaltung, die von der GIL bereitgestellt wurde.

Die GIL ist einfach zu implementieren und wurde leicht zu Python hinzugefügt. Es bietet eine Leistungssteigerung für Single-Thread-Programme, da nur eine Sperre verwaltet werden muss.

C-Bibliotheken, die nicht threadsicher waren, wurden einfacher zu integrieren. Und diese C-Erweiterungen wurden zu einem der Gründe, warum Python von verschiedenen Communities ohne weiteres übernommen wurde.

Wie Sie sehen, war die GIL eine pragmatische Lösung für ein schwieriges Problem, mit dem die CPython-Entwickler schon früh in Pythons Leben konfrontiert waren.

Die Auswirkungen auf Python-Programme mit mehreren Threads

Wenn Sie sich ein typisches Python-Programm oder ein anderes Computerprogramm ansehen, gibt es einen Unterschied zwischen denen, die in ihrer Leistung CPU-gebunden sind, und denen, die E / A-gebunden sind.

CPU-gebundene Programme sind solche, die die CPU an ihre Grenzen bringen. Dies schließt Programme ein, die mathematische Berechnungen wie Matrixmultiplikationen, Suchen, Bildverarbeitung usw. durchführen.

I/O-bound programs are the ones that spend time waiting for Input/Output which can come from a user, file, database, network, etc. I/O-bound programs sometimes have to wait for a significant amount of time till they get what they need from the source due to the fact that the source may need to do its own processing before the input/output is ready, for example, a user thinking about what to enter into an input prompt or a database query running in its own process.

Schauen wir uns ein einfaches CPU-gebundenes Programm an, das einen Countdown ausführt:

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)

Das Ausführen dieses Codes auf meinem System mit 4 Kernen ergab die folgende Ausgabe:

$ python single_threaded.py
Time taken in seconds - 6.20024037361145

Jetzt habe ich den Code ein wenig geändert, um denselben Countdown mit zwei parallelen Threads durchzuführen:

# multi_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time taken in seconds -', end - start)

Und als ich es wieder lief:

$ python multi_threaded.py
Time taken in seconds - 6.924342632293701

Wie Sie sehen können, dauert die Fertigstellung beider Versionen fast gleich lange. In der Multithread-Version verhinderte die GIL, dass die CPU-gebundenen Threads parallel ausgeführt werden.

Die GIL hat keinen großen Einfluss auf die Leistung von E / A-gebundenen Multithread-Programmen, da die Sperre von Threads gemeinsam genutzt wird, während diese auf E / A warten.

Ein Programm, dessen Threads vollständig an die CPU gebunden sind, z. B. ein Programm, das ein Bild in Teilen mithilfe von Threads verarbeitet, wird aufgrund der Sperre nicht nur zu einem einzelnen Thread, sondern es wird auch eine Verlängerung der Ausführungszeit angezeigt, wie im obigen Beispiel dargestellt im Vergleich zu einem Szenario, in dem es als Single-Thread geschrieben wurde.

Diese Erhöhung ist das Ergebnis von Overheads beim Erfassen und Freigeben, die durch die Sperre hinzugefügt werden.

Warum wurde die GIL noch nicht entfernt?

Die Entwickler von Python erhalten diesbezüglich viele Beschwerden, aber eine so beliebte Sprache wie Python kann keine so bedeutende Änderung bringen wie das Entfernen von GIL, ohne Probleme mit der Abwärtsinkompatibilität zu verursachen.

Die GIL kann offensichtlich entfernt werden, und dies wurde in der Vergangenheit mehrfach von den Entwicklern und Forschern durchgeführt, aber all diese Versuche haben die vorhandenen C-Erweiterungen zerstört, die stark von der von der GIL bereitgestellten Lösung abhängen.

Natürlich gibt es andere Lösungen für das Problem, das die GIL löst, aber einige von ihnen verringern die Leistung von Single-Threaded- und Multi-Threaded-I / O-gebundenen Programmen, und einige von ihnen sind einfach zu schwierig. Schließlich möchten Sie nicht, dass Ihre vorhandenen Python-Programme nach der Veröffentlichung einer neuen Version langsamer ausgeführt werden, oder?

Der Schöpfer und BDFL von Python, Guido van Rossum, gab der Community im September 2007 in seinem Artikel“It isn’t Easy to remove the GIL” eine Antwort:

"Ich würde eine Reihe von Patches in Py3konly if der Leistung für ein Single-Threaded-Programm (und für ein Multi-Threaded-Programm, aber E / A-gebundenes Programm)does not decrease begrüßen."

Und diese Bedingung wurde von keinem der seitdem unternommenen Versuche erfüllt.

Warum wurde es in Python 3 nicht entfernt?

Python 3 hatte die Möglichkeit, viele Funktionen von Grund auf neu zu starten, und brach dabei einige der vorhandenen C-Erweiterungen ab, für die Änderungen aktualisiert und portiert werden mussten, um mit Python 3 zu funktionieren. Dies war der Grund, warum die frühen Versionen von Python 3 von der Community langsamer angenommen wurden.

Aber warum wurde GIL nicht nebenan entfernt?

Das Entfernen der GIL hätte Python 3 im Vergleich zu Python 2 in der Single-Threaded-Leistung langsamer gemacht, und Sie können sich vorstellen, was dies zur Folge gehabt hätte. Sie können mit den Single-Threaded-Leistungsvorteilen der GIL nicht streiten. Das Ergebnis ist also, dass Python 3 immer noch die GIL hat.

Aber Python 3 hat die bestehende GIL erheblich verbessert -

Wir haben die Auswirkungen von GIL auf "nur CPU-gebundene" und "nur E / A-gebundene" Multithread-Programme diskutiert, aber was ist mit den Programmen, bei denen einige Threads E / A-gebunden und andere CPU-gebunden sind?

In solchen Programmen war bekannt, dass Pythons GIL die E / A-gebundenen Threads aushungert, indem es ihnen nicht die Möglichkeit gibt, die GIL von CPU-gebundenen Threads zu erhalten.

Dies lag an einem in Python integrierten Mechanismus, der Threads dazu zwang, die GILafter a fixed interval für die kontinuierliche Verwendung freizugeben. Wenn niemand anderes die GIL erwarb, konnte derselbe Thread seine Verwendung fortsetzen.

>>>

>>> import sys
>>> # The interval is set to 100 instructions:
>>> sys.getcheckinterval()
100

Das Problem bei diesem Mechanismus war, dass der CPU-gebundene Thread die GIL die meiste Zeit selbst erneut abrief, bevor andere Threads sie erwerben konnten. Dies wurde von David Beazley untersucht und Visualisierungen könnenhere gefunden werden.

Dieses Problem wurde 2009 in Python 3.2 von Antoine Pitrou behoben, deradded a mechanismdie Anzahl der GIL-Erfassungsanforderungen anderer Threads überprüft hat, die gelöscht wurden, und dem aktuellen Thread nicht erlaubt hat, GIL erneut abzurufen, bevor andere Threads ausgeführt werden konnten .

Wie man mit Pythons GIL umgeht

Wenn die GIL Ihnen Probleme bereitet, können Sie hier einige Ansätze ausprobieren:

Multi-processing vs multi-threading: Die beliebteste Methode ist die Verwendung eines Multi-Processing-Ansatzes, bei dem Sie mehrere Prozesse anstelle von Threads verwenden. Jeder Python-Prozess erhält einen eigenen Python-Interpreter und Speicherplatz, sodass die GIL kein Problem darstellt. Python verfügt über einmultiprocessing-Modul, mit dem wir Prozesse wie folgt erstellen können:

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n>0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds -', end - start)

Das Ausführen auf meinem System ergab folgende Ausgabe:

$ python multiprocess.py
Time taken in seconds - 4.060242414474487

Eine ordentliche Leistungssteigerung im Vergleich zur Multithread-Version, oder?

Die Zeit ist nicht auf die Hälfte der oben genannten Zeit gesunken, da das Prozessmanagement seine eigenen Gemeinkosten hat. Mehrere Prozesse sind schwerer als mehrere Threads. Beachten Sie daher, dass dies zu einem Skalierungsengpass führen kann.

Alternative Python interpreters: Python verfügt über mehrere Interpreter-Implementierungen. CPython, Jython, IronPython und PyPy, geschrieben in C, Java, C # und Python, sind die beliebtesten. GIL existiert nur in der ursprünglichen Python-Implementierung, die CPython ist. Wenn Ihr Programm mit seinen Bibliotheken für eine der anderen Implementierungen verfügbar ist, können Sie sie auch ausprobieren.

Just wait it out: Während viele Python-Benutzer die Single-Threaded-Leistungsvorteile von GIL nutzen. Die Multithreading-Programmierer müssen sich keine Sorgen machen, da einige der klügsten Köpfe in der Python-Community daran arbeiten, die GIL aus CPython zu entfernen. Ein solcher Versuch ist alsGilectomy bekannt.

Die Python GIL wird oft als mysteriöses und schwieriges Thema angesehen. Beachten Sie jedoch, dass Sie als Pythonista normalerweise nur betroffen sind, wenn Sie C-Erweiterungen schreiben oder wenn Sie in Ihren Programmen CPU-gebundenes Multithreading verwenden.

In diesem Fall sollte dieser Artikel Ihnen alles geben, was Sie brauchen, um zu verstehen, was die GIL ist und wie Sie in Ihren eigenen Projekten damit umgehen können. Und wenn Sie das Innenleben von GIL auf niedriger Ebene verstehen möchten, empfehlen wir Ihnen, den Vortrag von David Beazley inUnderstanding the Python GILzu sehen.