Heim  >  Artikel  >  Backend-Entwicklung  >  Was ist die Multithreading-Leistung von Python GIL? Eine ausführliche Erklärung von GIL

Was ist die Multithreading-Leistung von Python GIL? Eine ausführliche Erklärung von GIL

PHPz
PHPzOriginal
2017-04-23 17:08:031236Durchsuche

Vorwort: Blogger hören oft das Wort GIL, wenn sie zum ersten Mal mit Python in Kontakt kommen, und stellen fest, dass dieses Wort oft mit der Unfähigkeit von Python gleichgesetzt wird, Multithreading effizient zu implementieren. Im Einklang mit der Forschungshaltung, nicht nur zu wissen, was passiert, sondern auch zu wissen, warum es passiert, sammelte der Blogger alle Arten von Informationen, verbrachte innerhalb einer Woche ein paar Stunden Freizeit, um GIL tiefgreifend zu verstehen, und fasste sie hier zusammen Ich hoffe auch, dass die Leser GIL durch diesen Artikel besser und objektiver verstehen können.

Was ist GIL?

Das erste, was klargestellt werden muss, ist, dass GIL keine Funktion von Python ist. Es wurde bei der Implementierung von Python eingeführt Parser (CPython) ein Konzept. Genau wie C++ handelt es sich um eine Reihe von Sprach-(Grammatik-)Standards, die jedoch mit verschiedenen Compilern in ausführbaren Code kompiliert werden können. Berühmte Compiler wie GCC, INTEL C++, Visual C++ usw. Das Gleiche gilt für Python. Derselbe Code kann über verschiedene Python-Ausführungsumgebungen wie CPython, PyPy und Psyco ausgeführt werden. Beispielsweise verfügt JPython nicht über GIL. Allerdings ist CPython in den meisten Umgebungen die Standard-Python-Ausführungsumgebung. Daher ist CPython in der Vorstellung vieler Menschen Python, und sie gehen davon aus, dass GIL auf die Mängel der Python-Sprache zurückzuführen ist. Lassen Sie uns hier klarstellen: GIL ist keine Funktion von Python. Python muss sich überhaupt nicht auf GIL verlassen.

Was ist also GIL in der CPython-Implementierung? Der vollständige Name von GIL Global Interpreter Lock Um Irreführungen zu vermeiden, werfen wir einen Blick auf die offizielle Erklärung:

In CPython ist die globale Interpretersperre oder GIL ein Mutex, der mehrere native verhindert Threads daran gehindert werden, Python-Bytecodes gleichzeitig auszuführen. Diese Sperre ist vor allem deshalb notwendig, weil die Speicherverwaltung von CPython nicht threadsicher ist >

Okay, sieht es nicht schlecht aus? Ein Mutex, der verhindert, dass mehrere Threads gleichzeitig Maschinencode ausführen, scheint eine globale Sperre zu sein, die auf den ersten Blick wie ein Fehler aussieht! Machen Sie sich keine Sorgen, wir werden es im Folgenden langsam analysieren.

Warum gibt es GIL?

Aufgrund physischer Einschränkungen wurde der Wettbewerb zwischen CPU-Herstellern hinsichtlich der Kernfrequenz durch Multi-Core ersetzt. Um die Leistung von Mehrkernprozessoren effektiver zu nutzen, sind Multithread-

Programmiermethoden

entstanden, die die Schwierigkeiten der Datenkonsistenz und Status-Synchronisation zwischen Threads mit sich bringen. Sogar der Cache innerhalb der CPU ist keine Ausnahme. Um die Datensynchronisation zwischen mehreren Cache effektiv zu lösen, haben verschiedene Hersteller viel Aufwand betrieben, was zwangsläufig gewisse Auswirkungen auf die Leistung hat Verlust. Natürlich kann Python nicht entkommen. Um die Vorteile mehrerer Kerne zu nutzen, begann Python, Multithreading zu unterstützen.

Der einfachste Weg, die Datenintegrität und Statussynchronisation zwischen mehreren Threads zu lösen, ist natürlich das Sperren.

Es gibt also die super große Sperre von GIL, und wenn immer mehr Codebasisentwickler diese Einstellung akzeptieren, beginnen sie, sich stark auf diese Funktion zu verlassen (d. h. Standard-Python interne Objekte ist threadsicher, keine Notwendigkeit, zusätzliche Speichersperren und Synchronisierungsvorgänge während der Implementierung zu berücksichtigen). Langsam stellte sich heraus, dass diese Implementierungsmethode schmerzhaft und ineffizient war. Aber als alle versuchten, die GIL aufzuspalten und zu entfernen, stellten sie fest, dass sich eine große Anzahl von Bibliothekscodeentwicklern stark auf die GIL verlassen hatte und es sehr schwierig war, sie zu entfernen. Wie schwierig ist es? Um eine Analogie zu geben: Ein „kleines Projekt“ wie

MySQL

brauchte fast 5 Jahre, um die große Sperre von Buffer Pool Mutex in verschiedene kleine Sperren aufzuteilen, von 5,5 über 5,6 auf 5,7 Jahre und dauert immer noch. MySQL, ein Produkt mit Unternehmensunterstützung und einem festen Entwicklungsteam dahinter, hat es so schwer, ganz zu schweigen von einem stark gemeinschaftsbasierten Team aus Kernentwicklern und Code-Mitwirkenden wie Python? Vereinfacht ausgedrückt hat die Existenz von GIL eher historische Gründe. Wenn wir alles noch einmal machen müssten, stünden wir immer noch vor dem Problem des Multithreadings, aber zumindest wäre es eleganter als der aktuelle GIL-Ansatz.

Die Auswirkungen von GIL

Aus der obigen Einführung und der offiziellen Definition ist GIL zweifellos ein weltweit exklusives Schloss. Es besteht kein Zweifel, dass die Existenz globaler Sperren einen großen Einfluss auf die Effizienz von Multithreading haben wird. Es ist fast so, als ob Python ein Single-Thread-Programm wäre. Dann werden die Leser sagen, dass die Effizienz nicht schlecht sein wird, solange die globale Sperre aufgehoben wird. Solange die GIL bei zeitaufwändigen E/A-Vorgängen freigegeben werden kann, kann die Betriebseffizienz noch verbessert werden. Mit anderen Worten, egal wie schlimm es ist, es wird nicht schlechter sein als die Effizienz eines einzelnen Threads. Das stimmt in der Theorie, aber in der Praxis? Python ist schlimmer als Sie denken.

Vergleichen wir die Effizienz von Python beim Multithreading und Singlethreading. Die Testmethode ist sehr einfach, eine Zähler- Funktion , die 100 Millionen Mal durchläuft. Einer wird zweimal durch einen einzelnen Thread ausgeführt, und einer wird durch mehrere Threads ausgeführt. Vergleichen Sie abschließend die Gesamtausführungszeit. Die Testumgebung ist ein Dual-Core Mac Pro. Hinweis: Um die Auswirkungen des Leistungsverlusts der Thread-Bibliothek selbst auf die Testergebnisse zu verringern, verwendet der Single-Thread-Code hier auch Threads. Führen Sie es einfach zweimal nacheinander aus, um einen einzelnen Thread zu simulieren.

Einzelner Thread, der sequentiell ausgeführt wird (single_thread.py)

#! /usr/bin/pythonfrom threading import Threadimport timedef my_counter():    i = 0    for _ in range(100000000):        i = i + 1    return Truedef main():    thread_array = {}    start_time = time.time()    for tid in range(2):        t = Thread(target=my_counter)        t.start()        t.join()    end_time = time.time()    print("Total time: {}".format(end_time - start_time))if name == 'main':    main()
Zwei gleichzeitige Threads, die gleichzeitig ausgeführt werden (multi_thread.py)

#! /usr/bin/pythonfrom threading import Threadimport timedef my_counter():    i = 0    for _ in range(100000000):        i = i + 1    return Truedef main():    thread_array = {}    start_time = time.time()    for tid in range(2):        t = Thread(target=my_counter)        t.start()        thread_array[tid] = t    for i in range(2):        thread_array[i].join()    end_time = time.time()    print("Total time: {}".format(end_time - start_time))if name == 'main':    main()
Der Das Bild unten zeigt das Testergebnis

Was ist die Multithreading-Leistung von Python GIL? Eine ausführliche Erklärung von GIL

Sie können sehen, dass Python im Multithread-Modus tatsächlich 45 % langsamer ist als ein einzelner Thread. Laut der vorherigen Analyse sollte serialisiertes Multithreading selbst bei Vorhandensein einer globalen GIL-Sperre die gleiche Effizienz wie Singlethreading haben. Wie konnte es also zu einem so schlechten Ergebnis kommen?

Lassen Sie uns die Gründe dafür anhand der Implementierungsprinzipien von GIL analysieren.

Mängel des aktuellen GIL-Designs

Planungsmethode basierend auf der Anzahl der Pcodes

Nach den Vorstellungen der Python-Community die Thread-Planung Das Betriebssystem selbst ist bereits sehr ausgereift. Sobald es stabil ist, besteht keine Notwendigkeit, ein eigenes zu erstellen. Ein Python-Thread ist also ein Pthread der

C-Sprache und wird über den Planungsalgorithmus des Betriebssystems geplant (Linux ist beispielsweise CFS). Damit jeder Thread die CPU-Zeit gleichmäßig nutzen kann, berechnet Python die Anzahl der aktuell ausgeführten Mikrocodes und erzwingt die Freigabe der GIL, wenn sie einen bestimmten Schwellenwert erreicht. Zu diesem Zeitpunkt wird auch die Thread-Planung des Betriebssystems ausgelöst (ob der Kontextwechsel tatsächlich durchgeführt wird, wird natürlich vom Betriebssystem bestimmt).

Pseudocode

while True:    acquire GIL    for i in 1000:        do something    release GIL    /* Give Operating System a chance to do thread scheduling */
Dieser Modus ist kein Problem, wenn nur ein CPU-Kern vorhanden ist. Jeder Thread kann die GIL erfolgreich abrufen, wenn er aktiviert wird (da die Thread-Planung nur erfolgt, wenn die GIL freigegeben wird). Wenn die CPU jedoch über mehrere Kerne verfügt, treten Probleme auf. Wie Sie dem Pseudocode entnehmen können, gibt es zwischen

und release GIL fast keine Lücke. Wenn also andere Threads auf anderen Kernen aktiviert werden, hat der Hauptthread in den meisten Fällen die GIL erneut erworben. Zu diesem Zeitpunkt kann der zur Ausführung aktivierte Thread nur vergeblich CPU-Zeit verschwenden und zusehen, wie ein anderer Thread glücklich mit GIL ausgeführt wird. Nach Erreichen der Schaltzeit geht es dann in den Wartezustand, wird erneut geweckt und wartet erneut, wodurch sich der Teufelskreis wiederholt. acquire GIL

PS: Natürlich ist diese Implementierung primitiv und hässlich. Die Interaktion zwischen GIL und Thread-Planung wird in jeder Python-Version schrittweise verbessert. Versuchen Sie beispielsweise zunächst, die GIL zu halten, während Sie den Thread-Kontextwechsel durchführen, die GIL freizugeben, während Sie auf E/A warten usw. Was jedoch nicht geändert werden kann, ist, dass die Existenz von GIL den ohnehin schon teuren Betrieb der Betriebssystem-Thread-Planung noch luxuriöser macht. Erweiterte Lektüre zu den Auswirkungen von GIL

Um die Leistungsauswirkungen von GIL auf Multithreading intuitiv zu verstehen, finden Sie hier direkt ein Testergebnisdiagramm (siehe Bild unten). Die Abbildung zeigt die Ausführung von zwei Threads auf einer Dual-Core-CPU. Bei beiden Threads handelt es sich um CPU-intensive Rechenthreads. Der grüne Teil zeigt an, dass der Thread ausgeführt wird und nützliche Berechnungen durchführt. Der rote Teil zeigt den Zeitpunkt an, zu dem der Thread aufwachen sollte, die GIL jedoch nicht abrufen und keine effektiven Berechnungen durchführen konnte.

Wie aus der Abbildung ersichtlich ist, führt die Existenz von GIL dazu, dass Multithreading die GIL Performancegleichzeitigen Verarbeitungsfunktionen von Multi-Core-CPUs nicht vollständig nutzen kann.

Können Pythons IO-intensive Threads also von Multithreading profitieren? Werfen wir einen Blick auf die Testergebnisse unten. Die Bedeutung der Farben ist die gleiche wie im Bild oben. Der weiße Teil zeigt an, dass der E/A-Thread wartet. Es ist ersichtlich, dass der E/A-Thread, wenn er das Datenpaket empfängt und das Terminal zum Umschalten veranlasst, aufgrund des Vorhandenseins eines CPU-intensiven Threads immer noch nicht in der Lage ist, die GIL-Sperre zu erhalten, was zu einer endlosen Warteschleife führt.

GIL IO Performance

Eine einfache Zusammenfassung lautet: Pythons Multi-Threading auf Multi-Core-CPUs wirkt sich nur dann positiv auf IO-intensive Berechnungen aus, wenn mindestens ein CPU-intensiver Thread vorhanden ist, die Multi-Thread-Effizienz Wird aufgrund von GIL deutlich sinken.

Wie man vermeidet, von GIL betroffen zu sein

Nachdem ich so viel gesagt habe: Wenn ich die Lösung nicht erwähne, handelt es sich nur um einen populärwissenschaftlichen Beitrag, aber das ist er nutzlos. GIL ist so schlimm, gibt es einen Ausweg? Werfen wir einen Blick auf die verfügbaren Lösungen.

Verwenden Sie Multiprocessing, um Thread zu ersetzen

Das Aufkommen der Multiprocessing-Bibliothek dient größtenteils dazu, die Ineffizienz der Thread-Bibliothek aufgrund von GIL auszugleichen. Es repliziert vollständig eine Reihe von Schnittstellen, die vom Thread bereitgestellt werden, um die Migration zu erleichtern. Der einzige Unterschied besteht darin, dass mehrere Prozesse anstelle mehrerer Threads verwendet werden. Jeder Prozess verfügt über seine eigene unabhängige GIL, sodass es keinen GIL-Konflikt zwischen Prozessen gibt.

Natürlich ist Multiprocessing kein Allheilmittel. Seine Einführung wird die Schwierigkeit der Programmimplementierung der Datenkommunikation und Synchronisation zwischen Zeitthreads erhöhen. Nehmen wir den Zähler als Beispiel. Wenn wir möchten, dass mehrere Threads dieselbe -Variable akkumulieren, deklarieren Sie für Thread eine globale Variable und umschließen Sie drei Zeilen mit dem Thread.Lock-Kontext. Da Prozesse bei der Mehrfachverarbeitung die Daten anderer nicht sehen können, können sie nur eine Warteschlange im Hauptthread deklarieren, diese ablegen und dann abrufen oder gemeinsam genutzten Speicher verwenden. Diese zusätzlichen Implementierungskosten machen das Codieren von Multithread-Programmen, das ohnehin schon sehr mühsam ist, noch mühsamer. Was sind die spezifischen Schwierigkeiten? Interessierte Leser können diesen Artikel weiterlesen

Andere Parser verwenden

Wie bereits erwähnt, sind andere Parser besser, da GIL nur ein Produkt von CPython ist? Ja, Parser wie JPython und IronPython benötigen aufgrund der Art ihrer Implementierungssprachen nicht die Hilfe der GIL. Durch die Verwendung von Java/C# für die Parser-Implementierung verloren sie jedoch auch die Möglichkeit, die vielen nützlichen Funktionen der C-Sprachmodule der Community zu nutzen. Daher waren diese Parser schon immer eine relativ Nische. Schließlich wird jeder in der Anfangsphase Ersteres der Funktion vorziehen, Done is better than perfect.

Also ist es hoffnungslos?

Natürlich arbeitet die Python-Community auch sehr hart daran, die GIL kontinuierlich zu verbessern und versucht sogar, die GIL zu entfernen. Und in jeder Nebenversion gab es viele Verbesserungen. Interessierte Leser können diese Folie weiter lesen. Eine weitere Verbesserung: Überarbeitung der GIL – Änderung der Umschaltgranularität von Opcode-Zählung auf Zeitscheibenzählung – Vermeiden, dass der Thread, der kürzlich die GIL-Sperre freigegeben hat, sofort wieder geplant wird – NeuThreadPrioritätFunktion (Threads mit hoher Priorität können andere Threads dazu zwingen, die von ihnen gehaltenen GIL-Sperren freizugeben)

Zusammenfassung

Python GIL ist eigentlich eine Kombination aus Funktionalität und Leistung. Es ist das Produkt Es handelt sich um einen Kompromiss zwischen Zeit und Raum, insbesondere um die Rationalität seiner Existenz, und es gibt auch objektive Faktoren, die schwer zu ändern sind. Aus der Analyse dieses Teils können wir die folgenden einfachen Schlussfolgerungen ziehen: - Aufgrund der Existenz von GIL erzielen im IO-Bound-Szenario nur mehrere Threads eine bessere Leistung. - Wenn Sie mit hoher paralleler Rechenleistung programmieren möchten, können Sie dies in Betracht ziehen Verwenden des Kerns Einige davon werden auch in C-Module umgewandelt oder einfach in anderen Sprachen implementiert – GIL wird noch lange bestehen bleiben, aber es wird weiter verbessert

Referenz

Pythons schwierigstes Problem Offizielle Dokumente zu GIL Überarbeitung der Thread-Prioritäten und der neuen GIL


Das obige ist der detaillierte Inhalt vonWas ist die Multithreading-Leistung von Python GIL? Eine ausführliche Erklärung von GIL. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn