Ich habe versucht, so viel wie möglich über GIL herauszufinden, das Sie wissen sollten, wenn Sie parallel mit Python arbeiten

Ich hatte kürzlich die Gelegenheit, eine Orchestration Layer (BFF) -Anwendung in Python zu schreiben.

Asyncio wurde aus Python 3.4 eingeführt, und obwohl die E / A-gebundene Verarbeitung auch mit einem einzelnen Thread effizient gehandhabt werden kann, ist GIL für die CPU-gebundene Verarbeitung weiterhin vorhanden, sodass die parallele Verarbeitung unter einem einzigen Prozess durchgeführt werden kann. Es wird eingeschränkt.

Daraus ist ersichtlich, dass es für die Behandlung mehrerer E / A-gebundener Prozesse geeignet ist, anstatt CPU-gebunden als Sprachmerkmal. Es ist ein wichtiger Faktor bei der Auswahl einer Sprache, aber ich dachte, dass es notwendig ist, den Mechanismus von GIL zu diesem Zweck erneut zu kennen, also habe ich ihn untersucht.

Was ist GIL (Global Interpolator Lock)?

Was ist GIL überhaupt?

Formal als Global Interpreter Lock bezeichnet, ist es ein exklusiver Sperrmechanismus, der in Sprachen wie Python und Ruby zu finden ist. Allein die Betrachtung dieser beiden Sprachen scheint für dynamisch typisierte Sprachen charakteristisch zu sein, sie sind jedoch eher an der Koordination mit der C-Sprache beteiligt.

In Python ist CPython, das in der Sprache C implementiert ist, weit verbreitet.

Wenn Sie vor der Erläuterung von GIL erklären, dass "GIL in Python vorhanden ist", liegt ein Missverständnis vor. Lassen Sie es uns daher etwas genauer betrachten.

Es gibt in erster Linie mehrere Implementierungen der Sprache Python. Am weitesten verbreitet ist CPython, das in C implementiert ist. Dies wird wahrscheinlich implizit erwähnt, wenn die Sprachmerkmale von Python beschrieben werden.

Andere typische Beispiele sind in Java implementiertes Jython und IronPython, die auf dem .Net Framework ausgeführt werden, diese haben jedoch keine GIL. Warum wird CPython verwendet, wenn Sie das alleine hören? Möglicherweise denken Sie, dass wichtige Bibliotheken wie NumPy häufig in C implementiert sind und CPython aufgrund der Häufigkeit der Aktualisierungen der Sprachimplementierung häufig verwendet wird.

Basierend auf diesen wird GIL basierend auf den CPython-Spezifikationen erklärt.

Über die exklusive Sperre von GIL

Kehren wir nun zum Hauptthema zurück und gehen wir auf die Erklärung von GIL ein, aber grob gesagt ** "Bytecode kann auch von einem einzelnen Thread mit einer Sperre ausgeführt werden, selbst unter mehreren Threads, und andere Threads befinden sich im Standby-Zustand" ** Das ist. Die Sperre wird in regelmäßigen Abständen aufgehoben, und ein anderer Thread, der die Sperre neu erwirbt, führt das Programm aus.

Der Sperrmechanismus wird später beschrieben, aber vorerst sollte erkannt werden, dass die CPU-gebundene Verarbeitung nur von einem Thread ausgeführt werden kann, was die Parallelisierung der Verarbeitung einschränkt.

Lassen Sie uns tatsächlich die Wirkung von GIL im Code sehen. Führen Sie zunächst ein einfaches Programm aus, das eine große Anzahl zählt.

countdown.py


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

if __name__ == '__main__':
    start = datetime.now()

    t1 = Thread(target=countdown)
    t2 = Thread(target=countdown)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

    end = datetime.now()
    print(f"Time: {end - start}")

Es ist ein Programm, das mit 2 Threads 10 Millionen herunterzählt, aber die Ausführungszeit in meiner Umgebung betrug ungefähr 1,1 Sekunden. Was ist also mit einem Thread?

if __name__ == '__main__':
    start = datetime.now()
    countdown()
    end = datetime.now()
    print(f"Time: {end - start}")

Dies endete in ungefähr 0,53 Sekunden. Ungefähr die Hälfte der beiden Threads bedeutet, dass nicht jeder Thread parallel läuft. Dies liegt daran, dass die CPU-gebundene Verarbeitung nur von einem Thread ausgeführt werden kann.

Aber was ist mit nicht CPU-gebundener Verarbeitung? Ersetzen Sie den Countdown durch Sleep und versuchen Sie, ihn in 2 Threads auszuführen.

sleep.py


def sleep():
    time.sleep(2.0)

Zu diesem Zeitpunkt ist der Vorgang in ca. 2 Sekunden abgeschlossen. Wenn es CPU-gebunden ist, dauert es 4 Sekunden in 2 x 2 Sekunden, aber wenn es im Ruhezustand ist, dauert es halbe 2 Sekunden. Dies liegt daran, dass die Sperre aufgehoben wurde, als der Ruhezustand ausgeführt wurde, und der wartende Thread unmittelbar danach in den Ruhezustand ging, der im Wesentlichen parallel verarbeitet wurde.

Übrigens treten beim Ausführen von Python-Bytecodes Sperren auf, nicht unbedingt bei Verwendung der CPU.

Warum existiert GIL?

Warum gibt es also eine GIL, die die Parallelverarbeitung überhaupt einschränkt? Dies ist keine Lösung, die ich durch Lesen des CPython-Codes selbst abgeleitet habe, aber das Folgende scheint die Hauptfaktoren zu sein.

Aus den oben genannten Gründen muss zum Ausführen von CPython nur ein Thread Bytecode ausführen können, und es gibt einen Mechanismus namens GIL, um dies zu realisieren.

Dies ist jedoch kein Merkmal der Sprache selbst, Python, sondern wird mit CPython assoziiert, das in C implementiert ist. In Java implementiertes Jython verfügt beispielsweise über keine GIL, da dank der Thread-Verwaltung durch JVM auch beim Multithreading kein Konflikt besteht.

CPython wird jedoch wahrscheinlich häufig verwendet, wahrscheinlich weil beurteilt wird, dass die Vorteile der Verwendung von C-Sprachressourcen und aktiven Updates größer sind als die Vorteile der Vermeidung von GIL.

GIL eröffnen und erwerben

(Die Beschreibung dieses Elements basiert auf Grundlegendes zur Python-GIL)

Der GIL-Mechanismus von CPython wurde mit Version 3.2 geändert, beginnend mit einer Sperrfreigabeanforderung mit dem Namen ** gil_drop_request **.

Wenn beispielsweise nur ein Thread vorhanden ist, wird die Ausführung fortgesetzt, bis ein einzelner Thread die Verarbeitung abgeschlossen hat. Dies liegt daran, dass die Entsperranforderung von nirgendwo angekommen ist.

Auf der anderen Seite ist es anders, wenn es mehrere Threads gibt. Suspend-Threads warten standardmäßig 5 ms und setzen dann "gil_drop_request" auf "1". Der laufende Thread gibt dann die Sperre auf und signalisiert dies.

スクリーンショット 2019-11-09 11.04.04.jpg

Wenn ein Thread, der auf eine Sperre wartet, das Signal empfängt, erhält er die Sperre, sendet jedoch zu diesem Zeitpunkt ein Signal, um zu informieren, dass er sie erfasst hat. Der Thread, der die Sperre früher aufgehoben hat, empfängt das Signal und wechselt in den angehaltenen Zustand.

スクリーンショット 2019-11-09 11.04.33.jpg (* Alle Bilder stammen aus Understanding the Python GIL)

Nach dem Timeout wiederholen mehrere Threads die Erfassung und Freigabe der Sperre auf die gleiche Weise wie zuvor und versuchen, die Sperre erneut zu aktivieren, indem Sie "gil_drop_request" setzen.

Timeout-Zeit kann geändert werden

Der Thread, der auf die Sperre wartet, wartet standardmäßig 5 ms. Dies können Sie der Zeit aus dem Python-Code "sys.getcheckinterval ()" entnehmen. Sie können die Intervallzeit auch mit sys.setcheckinterval (time) ändern.

Warum wurde es zu einer Methode zum Senden einer Sperrfreigabeanforderung?

Ab Python 3.2 wird die Sperre von gil_drop_request freigegeben, aber zuvor wurde die Sperre pro Ausführungseinheit namens tick freigegeben.

Dies kann übrigens von sys.getcheckinterval () referenziert werden, aber da es aufgrund der Änderung der Sperrmethode nicht mehr verwendet wird, wird die folgende Warnmeldung angezeigt.

DeprecationWarning: sys.getcheckinterval() and sys.setcheckinterval() are deprecated.  Use sys.getswitchinterval() instead.

Warum hat sich die Sperrfreigabemethode geändert?

Wie bereits erwähnt, sendet der wartende Thread jetzt eine Sperrfreigabeanforderung, aber zuvor hat der laufende Thread die Sperre standardmäßig nach 100 Ticks Ausführungseinheiten freigegeben. Dies hat jedoch einige Probleme in einer Multi-Core-Situation.

Schauen wir uns zunächst den Single-Core-Fall an. Wenn ein laufender Thread eine Sperre aufhebt, sendet er ein Signal an einen der wartenden Threads. Der Thread, der das Signal empfängt, wird in die Warteschlange gestellt, die auf die Ausführung wartet. Der OS-Scheduler wählt jedoch basierend auf der Priorität aus, ob der Thread, der gerade die Sperre aufgehoben hat, oder der Thread, der das Signal empfangen hat, als nächstes ausgeführt wird. Machen.

(Derselbe Thread kann nacheinander Sperren erwerben, was angesichts des Overheads der Kontextumschaltung wünschenswert sein kann.)

Im Fall von Multi-Core, sowohl für ausführbare Threads als auch für mehrere, die versuchen, die Sperre zu erlangen, kann man die Sperre jedoch nicht erlangen. Der Versuch, eine Sperre unnötig zu erwerben, ist selbst ein Aufwand, und das Problem besteht darin, dass wartende Threads kaum eine Sperre erwerben können.

Wartende Threads haben eine Zeitverzögerung, bevor sie fortgesetzt werden. Daher ist es wahrscheinlich, dass der gerade freigegebene Thread die Sperre bereits erhalten hat, wenn Sie versuchen, sie abzurufen. Es scheint, dass ein Thread die Sperre in einem langen Prozess länger als zehn Minuten aufrechterhalten kann.

Darüber hinaus besteht in Fällen, in denen häufig eine E / A-Verarbeitung auftritt, die aufgrund der Pufferung des Betriebssystems sofort endet, der Nachteil, dass die Last zunimmt, da Sperren bei jedem Warten auf E / A nacheinander freigegeben und erfasst werden. ..

Angesichts der oben genannten Probleme ist die derzeitige Methode zum Senden von Anforderungen durch Warten auf Threads besser.

Nachteile der aktuellen GIL

Wenn es also ein Problem mit der aktuellen GIL gibt, ist dies nicht der Fall. Das Material Grundlegendes zur Python-GIL führt zwei Nachteile ein.

1. Es kann zu einer unlauteren Schlosserfassung kommen

Erstens, wenn es drei oder mehr Threads gibt, kann der Thread, der die Sperrfreigabe angefordert hat, die Sperre möglicherweise nicht erwerben und wird möglicherweise von dem verzögerten Thread übernommen.

スクリーンショット 2019-11-09 12.23.39.jpg (* Zitiert aus Grundlegendes zur Python-GIL)

In der Abbildung oben fordert Thread 2 nach einer Zeitüberschreitung die Sperrfreigabe an, und Thread 1 signalisiert auch die Sperrfreigabe. Ursprünglich sollte Thread 2 die Sperre erhalten haben, aber in der Zwischenzeit wurde Thread 3 später in die Warteschlange gestellt, um die Sperre bevorzugt zu erhalten.

Auf diese Weise kann abhängig vom Timing die Sperrenerfassung auf einen bestimmten Thread vorgespannt werden und die parallele Verarbeitung kann ineffizient werden.

2. Ineffizienz kann aufgrund des Konvoieffekts auftreten

Wenn ein CPU-gebundener Thread und ein E / A-gebundener Thread gleichzeitig ausgeführt werden, kann ein ineffizienter Status namens Convoy-Effekt auftreten.

Aus Sicht des gesamten Prozesses erhalten E / A-gebundene Threads die Priorität, Sperren zu halten. Wenn E / A-Wartezeiten auftreten, werden sie in CPU-gebundene Threads verschoben, und wenn die E / A abgeschlossen sind, erhalten sie erneut Prioritätssperren. Es ist effizient, es zu lassen. Wenn andererseits nur CPU-gebundene Threads Sperren haben, bleibt die E / A-gebundene Verarbeitung erhalten, und es ist einfacher zu verstehen, wenn man bedenkt, dass die Ausführungszeit um die Wartezeit für das Warten auf E / A verlängert wird.

Threads haben jedoch keine Priorität, sodass Sie nicht steuern können, welcher Thread die Sperre bevorzugt erhält. Wenn zwei Threads warten, erhält der CPU-gebundene Thread möglicherweise zuerst die Sperre.

Auch wenn die E / A sofort endet, muss bis zum Timeout gewartet werden. Wenn eine große Anzahl von E / A-Wartezeiten auftritt, wird die CPU-gebundene Verarbeitung möglicherweise beendet, während auf aufeinanderfolgende Zeitüberschreitungen gewartet wird, sodass nur E / A-Wartezeiten verbleiben.

Dies wird als "Konvoieffekt" bezeichnet. Da die Sperre jedoch erst nach einer Zeitüberschreitung aufgehoben werden muss, kann sie aus Sicht der Gesamtoptimierung ineffizient sein.

Führen Sie die parallele Verarbeitung in mehreren Prozessen durch

Wie viele von Ihnen wissen, kann die CPU-gebundene Verarbeitung parallel ausgeführt werden, indem sie mehrere Prozesse umfasst. Dies liegt daran, dass jeder Prozess einen Interpreter enthält und GIL auf Interpreterbasis existiert.

Versuchen wir, den Teil auszuführen, der zuvor in Multi-Thread in Multi-Thread verarbeitet wurde.

countdown.py


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

if __name__ == '__main__':
    start = datetime.now()

    t1 = Process(target=countdown)
    t2 = Process(target=countdown)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

    end = datetime.now()
    print(f"Time: {end - start}")

Der Prozess, der bei mehreren Prozessen ungefähr 1,1 Sekunden dauerte, beträgt jetzt bei mehreren Prozessen ungefähr 0,65 Sekunden. Sie sehen, dass es auch bei CPU-Bindung parallel ausgeführt werden kann.

Obwohl es einen höheren Overhead als Threads hat, kann es Werte zwischen Prozessen gemeinsam nutzen und ist nützlich, wenn die CPU-gebundene Verarbeitung parallel ausgeführt wird.

In Python 3.8 eingeführter Subinterpreter

Zum Zeitpunkt dieses Schreibens war der Subinterpreter vorläufig in dem gerade veröffentlichten Python 3.8 implementiert. Der Subinterpreter wird in PEP 554 vorgeschlagen, wurde jedoch noch nicht zusammengeführt.

Wie bereits erwähnt, existiert GIL auf Dolmetscherbasis, aber Unterinterpreter ermöglichen es, dass mehrere Dolmetscher im selben Prozess gehalten werden.

Es ist eine mögliche Idee für die Zukunft, aber da CPython in Runtime den Status hat, scheint es immer noch viele Probleme zu geben, den Status im Interpreter aufrechtzuerhalten.

Sie können es tatsächlich verwenden, indem Sie auf Python 3.8 aktualisieren und "_xxsubinterpreters" importieren. Auf Produktionsebene ist die Verwendung jedoch möglicherweise immer noch schwierig.

Nutzen Sie Event-Loops mit Asyncio

Es ist eine methodologische Geschichte, die vom Hauptpunkt der Erklärung von GIL abweicht, aber in dem Fall, in dem im aktuellen Python mehrere E / A-Wartezeiten auftreten, kann es praktischer sein, die Ereignisschleife von "asyncio" zu verwenden.

asyncio kommt den Vorteilen des Multithreading nahe, da durch E / A-Multiplexing mehrere E / A-Vorgänge effizient in einem einzigen Thread verarbeitet werden können.

Zusätzlich zum Speichern von Speicher im Vergleich zu Multithreading besteht keine Notwendigkeit, das Erfassen / Freigeben mehrerer Threads durch Sperren in Betracht zu ziehen, und native Kollouts durch Async / Warten können intuitiv geschrieben werden, wodurch die Denklast für den Programmierer verringert wird.

Ich werde Pythons Collouts in einem separaten Artikel ausführlich vorstellen.

Schließlich

Obwohl sich die Geschichte in der zweiten Hälfte verbreitete, wurden in diesem Artikel Themen im Zusammenhang mit GIL umfassend beschrieben.

Es ist vielleicht nicht sinnvoll, sich beim Schreiben von Code auf Anwendungsebene jeden Tag bewusst zu sein, aber ich dachte, dass es für die Sprachauswahl hilfreich wäre, zu wissen, welche Einschränkungen bei der parallelen Verarbeitung bestehen. Schreiben Sie also einen Artikel. Ich tat.

Referenzmaterial

Recommended Posts

Ich habe versucht, so viel wie möglich über GIL herauszufinden, das Sie wissen sollten, wenn Sie parallel mit Python arbeiten
Ich habe versucht herauszufinden, ob ReDoS mit Python möglich ist
Ich habe untersucht, wie der Arbeitsablauf mit Excel x Python ④ optimiert werden kann
Ich habe versucht herauszufinden, wie der Arbeitsablauf mit Excel x Python optimiert werden kann
Ich habe untersucht, wie der Arbeitsablauf mit Excel x Python optimiert werden kann
Ich habe untersucht, wie der Arbeitsablauf mit Excel x Python optimiert werden kann
Ich habe versucht, die Entropie des Bildes mit Python zu finden
Ich habe versucht, die Umrisse von Big Gorilla herauszufinden
Ich habe versucht herauszufinden, wie ich den Arbeitsablauf mit Excel × Python, meiner Artikelzusammenfassung ★, optimieren kann
Ich habe versucht, die Zusammenführungssortierung in Python mit möglichst wenigen Zeilen zu implementieren
Ich habe versucht, die Operationen zusammenzufassen, die wahrscheinlich mit numpy-stl verwendet werden
Ich habe Python verwendet, um mich über die Rollenauswahl der 51 "Yachten" in der Welt zu informieren.
Ich habe versucht, die Verarbeitungsgeschwindigkeit mit dplyr von R und pandas von Python zu vergleichen
Ich habe versucht, eine CSV-Datei mit Python zu berühren
Ich habe versucht, Soma Cube mit Python zu lösen
Ich habe versucht, das Problem mit Python Vol.1 zu lösen
Ich habe versucht zusammenzufassen, was der Python-starke Mann in der professionellen Nachbarschaft des Wettbewerbs tut
Ich habe versucht zu simulieren, wie sich die Infektion mit Python ausbreitet
Ich habe versucht, die Emotionen des gesamten Romans "Wetterkind" zu analysieren
Ich habe versucht, mit TensorFlow den Durchschnitt mehrerer Spalten zu ermitteln
[Python] Ich habe versucht, Tweets über Corona mit WordCloud zu visualisieren
Ich habe versucht, den Unterschied zwischen A + = B und A = A + B in Python herauszufinden
Eine Geschichte, die nicht funktioniert hat, als ich versucht habe, mich mit dem Python-Anforderungsmodul anzumelden
Ich habe versucht, die Anfängerausgabe des Ameisenbuchs mit Python zu lösen
Ich möchte das Wetter mit LINE bot feat.Heroku + Python wissen
[Python] Ein Memo, das ich versucht habe, mit Asyncio zu beginnen
Ich möchte wissen, ob Sie Python auf Mac ・ Iroha installieren
Ich habe versucht, die Effizienz der täglichen Arbeit mit Python zu verbessern
So finden Sie heraus, wann Sie das Java-Installationsverzeichnis nicht kennen
Python-Anfänger versuchten es herauszufinden
Ich habe versucht, eine Blockchain zu implementieren, die tatsächlich mit ungefähr 170 Zeilen funktioniert
Wenn Sie awsebcli in CircleCI aufnehmen möchten, geben Sie die Python-Version an
Tipps (Eingabe / Ausgabe), die Sie beim Programmieren von Wettbewerben mit Python2 kennen sollten
Mayungos Python Learning Episode 2: Ich habe versucht, Zeichen mit Variablen zu löschen
Ich habe versucht, den Authentifizierungscode der Qiita-API mit Python abzurufen.
Ich habe es mit den Top 100 PyPI-Paketen versucht.> Ich habe versucht, die auf Python installierten Pakete grafisch darzustellen
Ich habe versucht, die Standardrolle neuer Mitarbeiter mit Python zu optimieren
Ich habe versucht, den Text des Romans "Wetterkind" mit Word Cloud zu visualisieren
Tipps (Kontrollstruktur), die Sie beim Programmieren von Wettbewerben mit Python2 kennen sollten
Ich habe versucht, die Filminformationen der TMDb-API mit Python abzurufen
Tipps (Datenstruktur), die Sie beim Programmieren von Wettbewerben mit Python2 kennen sollten
[Feature Poem] Ich verstehe die funktionale Sprache nicht. Sie können sie mit Python verstehen: Teil 2 Die Funktion, die die Funktion generiert, ist erstaunlich.
Ich habe versucht, die Informationen der ASPX-Site, die mit Selenium IDE ausgelagert wird, so programmlos wie möglich abzurufen
Python Ich weiß nicht, wie ich den Druckernamen bekomme, den ich normalerweise benutze.
Ich habe gawk verwendet, um den Maximalwert für NF herauszufinden.
Ich habe versucht, mit der Bibliothek GiNZA zur Verarbeitung natürlicher Sprache eindeutige Ausdrücke zu extrahieren
[Python] Ich habe versucht, 100 frühere Fragen zu lösen, die Anfänger und Fortgeschrittene lösen sollten [Teil 5/22]
[Python] Ich habe versucht, 100 frühere Fragen zu lösen, die Anfänger und Fortgeschrittene lösen sollten [Teil 7/22]
Ich habe versucht, die Python-Bibliothek "pykakasi" zu verwenden, die Kanji in Romaji konvertieren kann.
Ich habe versucht zu erklären, wozu der Python-Generator so einfach wie möglich ist.
Ich habe versucht, die Pferde vorherzusagen, die mit LightGBM unter den Top 3 sein werden
[Python] Ich habe versucht, 100 frühere Fragen zu lösen, die Anfänger und Fortgeschrittene lösen sollten [Teil 4/22]
[Python] Ich habe versucht, 100 frühere Fragen zu lösen, die Anfänger und Fortgeschrittene lösen sollten [Teil 3/22].
Ich habe versucht, die Tweets von JAWS DAYS 2017 mit Python + ELK einfach zu visualisieren
[Python] Ich habe versucht, 100 frühere Fragen zu lösen, die Anfänger und Fortgeschrittene lösen sollten [Teil 1/22]
[Python] Ich habe versucht, den Typnamen als Zeichenfolge aus der Typfunktion abzurufen
[Python] Ich habe die gleiche Berechnung versucht wie die Vorhersage von LSTM von Grund auf [Keras]
[Python] Ich habe versucht, 100 frühere Fragen zu lösen, die Anfänger und Fortgeschrittene lösen sollten [Teil 6/22]