gh-116167: Deaktivierung des GIL erlauben
(github.com/python)- CPython PR #116338 hat die Änderung „Allow disabling the GIL with
PYTHON_GIL=0or-X gil=0“ inpython:maingemergt, die es ermöglicht, den GIL in free-threaded Builds zu deaktivieren - Um die Möglichkeit offenzuhalten, den GIL zur Laufzeit wieder zu aktivieren, werden GIL-bezogene Datenstrukturen wie üblich initialisiert; die Deaktivierung erfolgt stattdessen durch ein Flag beim Start, sodass
take_gil()unddrop_gil()frühzeitig zurückkehren - Erste Prüfungen zeigten, dass mit
PYTHON_GIL=0einige Tests und kleine Programme ohne Threads normal liefen und sehr grundlegende Thread-Programme manchmal funktionierten, die vollständige Test-Suite jedoch schnell intest_asyncioabstürzte - Im Review wurden Tests für
PYTHON_GIL, Dokumentation, die Option-X gilsowie die Abbildung insys.flagsergänzt; außerdem wurde die Verarbeitung so korrigiert, dassPYTHON_GIL=1die Aktivierung des GIL erzwingt - Die Folgearbeiten wurden in die Themen Reaktivierung des GIL beim Laden inkompatibler Erweiterungen und standardmäßige Deaktivierung des GIL aufgeteilt; diese Änderung ergänzt damit die Steuerungsoberfläche für den GIL im free-threaded Build von Python 3.13
Gemergte Änderung
- CPython PR #116338 behandelt die Änderung
gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 colesburyhat sie am 11. März 2024 inpython:maingemergt- Der Umfang der Änderung wird mit 12 Dateien, 163 hinzugefügten Zeilen und 1 gelöschten Zeile angegeben
- Die Ziel-Funktion ist keine allgemeine Build-Option, sondern eine Laufzeitoption zum Deaktivieren des GIL in free-threaded Builds
Wie der GIL deaktiviert wird
- In free-threaded Builds kann der GIL mit den folgenden Einstellungen deaktiviert werden
PYTHON_GIL=0-X gil=0
- Damit der GIL zur Laufzeit wieder aktiviert werden kann, werden alle GIL-bezogenen Datenstrukturen wie gewohnt initialisiert
- Die eigentliche Deaktivierung erfolgt durch das Setzen eines Flags beim Start
- Durch dieses Flag kehren
take_gil()unddrop_gil()frühzeitig zurück
- Durch dieses Flag kehren
- Während des Reviews wurde auch ein Commit ergänzt, der
enable_gilbeiPYTHON_GIL=1korrekt setzt
Tests und aktuelle Einschränkungen
- Mit der Einstellung
PYTHON_GIL=0wurden einige Tests und kleine Programme überprüft- Tests und kleine Programme ohne Thread-Nutzung funktionierten erwartungsgemäß
- Sehr grundlegende Thread-Programme funktionierten gelegentlich
- Die vollständige Test-Suite stürzte jedoch schnell ab; als Stelle wurde
test_asynciofestgehalten - Mit dem Befehl
!buildbot nogilwurden mehrfach Builder-Tests rund um NoGIL eingeplantx86-64 MacOS Intel ASAN NoGIL PRx86-64 MacOS Intel NoGIL PRARM64 MacOS M1 Refleaks NoGIL PRARM64 MacOS M1 NoGIL PRAMD64 Ubuntu NoGIL Refleaks PRAMD64 Ubuntu NoGIL PRAMD64 Windows Server 2022 NoGIL PR
Im Review ergänzter Umfang
corona10schlug vor, inLib/test/test_cmd_line.pyTests für die Umgebungsvariable zu ergänzen- Danach wurden die folgenden Commits hinzugefügt
Add test for PYTHON_GIL in test_cmd_lineSet enable_gil properly when PYTHON_GIL=1Don't add 'enable_gil' to test_embed in normal builds
colesburyhielt es für sinnvoll, die Umgebungsvariable direkt bei ihrer Einführung zu dokumentieren- Als Begründung führte er an, dass das Configure-Flag
--disable-gilbereits dokumentiert ist - Für die Dokumentation hielt er fest, dass die Variable nur in free-threaded Builds verfügbar ist,
0die Deaktivierung des GIL erzwingt,1die Aktivierung des GIL erzwingt und dass dies neu in Python 3.13 ist
- Als Begründung führte er an, dass das Configure-Flag
- Anschließend wurde der Commit
Document PYTHON_GIL environment variablehinzugefügt
Ergänzung der Option -X gil und finaler Merge
- Nach einer Diskussion auf Discord wurde entschieden, zusätzlich zur Umgebungsvariable auch eine
-X-Option aufzunehmen - Der PR-Titel wurde von einer Formulierung nur mit
PYTHON_GIL=0auf eine Variante erweitert, die auch-X gil=0einschließt - Die zusätzlichen Commits enthielten unter anderem Folgendes
Add -X gil option, add to sys.flags, modify test to cover env var… and optionFix link to -X gilFix PYTHON_GIL versionchanged lineClarify test_flags in normal builds
ericsnowcurrently,erlend-aasland,corona10undcolesburyhaben die Änderung freigegeben- Der Merge-Commit ist
2731913; nach dem Merge reagiertevstinnerauf die Änderung mit den Worten, sie sei „interessant und sehr beängstigend“
Folgearbeiten
- Als Folge-Issues wurden zwei Aufgaben getrennt erfasst
- Der aktuelle PR ändert nicht den Standardwert des GIL, sondern ermöglicht es Nutzern in free-threaded Builds, den GIL-Status per Umgebungsvariable oder
-X-Option zu steuern
1 Kommentare
Hacker-News-Kommentare
Für alle, die neugierig auf die no-GIL-Arbeit sind, hier noch ein paar zusätzliche Links: [0], [1]
[0] Multithreaded Python without the GIL
https://docs.google.com/document/d/18CXhDb1ygxg-YXNBJNzfzZsD...
[1] Github-Repo
https://github.com/colesbury/nogil
[0] https://peps.python.org/pep-0703/
[1] https://github.com/colesbury/nogil-3.12
Ich bin gespannt, wie viel schneller sich das normale Python noch machen lässt. Es gibt inzwischen so viele Tools, die dieses Problem abmildern wollen, dass dadurch auch das Wertversprechen von Python selbst herausgefordert wird
Als Tools für Geschwindigkeitsverbesserungen fallen mir Mojo, pytorch, triton, numba und taichi ein. Es gibt so viele Versuche, dieses Problem zu lösen, dass ich beim letzten Mal, als ich eines davon ausprobieren wollte, von der Auswahl völlig überwältigt war. Am Ende habe ich taichi gewählt; es war ziemlich interessant und einfach zu nutzen, aber sein Anwendungsbereich war etwas begrenzt
Taichi ist wirklich unterschätzt. Es läuft auf allen Plattformen einschließlich Metal, hat viele Beispiele und ist leicht zu programmieren. Vor allem integriert es sich in das Ökosystem, statt das bestehende Ökosystem zu ersetzen
https://github.com/taichi-dev
Hervorragende Demo-Videos dazu, was man mit Taichi machen kann: https://www.youtube.com/watch?v=oXRJoQGCYFg
https://www.youtube.com/watch?v=WNh4Q7-OSJs
https://www.taichi-lang.org/
Ich frage mich, warum das in https://peps.python.org/pep-0703/ beschriebene Verfahren des biased reference counting nur Single-Thread-Affinität vorsieht und beim Zugriff aus anderen Threads atomare Inkremente/Dekremente erfordert
Bei anderen Implementierungen, zum Beispiel mehreren Rust-Crates mit biased reference counting, habe ich gesehen, dass nur beim Verschieben in einen neuen Thread atomar erhöht wird; dieser Thread führt dann wieder nicht-atomare Inkremente/Dekremente aus, bis der Zähler erneut 0 erreicht, und macht am Ende ein atomares Dekrement. Ich frage mich, ob das daran liegt, dass dies als Ergänzung zu einem bestehenden System erfolgt, es also ein einzelnes PyObject gibt und man es nicht durch einen Verweis auf ein neues thread-lokales Objekt ersetzen kann
In Rust ist ein "move" zur Eigentumsübertragung Teil der Sprache, aber in C oder Python gibt es kein entsprechendes Konzept. Deshalb ist schwer zu bestimmen, wann Eigentum übertragen werden sollte und welcher Thread der neue Eigentümer sein sollte. Man könnte Heuristiken verwenden. Wenn man zum Beispiel ein Objekt in
queue.SimpleQueuelegt, könnte man das Eigentum aufgeben oder übertragen, aber selbst dann ist schwer im Voraus zu wissen, welcher Thread das Objekt aus der Queue per "get" holen wirdDer Leistungsgewinn dürfte auch klein sein. Viele Objekte werden nur von einem einzigen Thread verwendet, und manche Objekte werden zwar von mehreren Threads verwendet, aber Objekte, die erst ausschließlich von einem Thread und später ausschließlich von einem anderen Thread genutzt werden, sind selten
Erst die Nachricht über tranched bread, und jetzt auch noch das? Was für Zeiten
Ich fand es etwas schade, als das Unladen-Swallow-Projekt [1] im Sande verlief. Es ist schön zu sehen, dass Python wieder zu einem Kernpfad für Optimierungen zurückkehrt
[1] https://en.wikipedia.org/wiki/CPython#Unladen_Swallow
Ich hätte gern eine Erklärung, als wäre ich fünf
Ich verstehe konzeptionell, was der GIL ist. Aber welche Auswirkungen hat diese Änderung? Können wir jetzt allgemein Leistungssteigerungen erwarten, und gehen dabei Pakete kaputt?
Selbst wenn es nicht um intensive CPU-Arbeit geht, kann diese Änderung nützlich sein. In letzter Zeit wird viel Code mit den nativen asyncio-Sprachfunktionen von Python geschrieben. Das funktioniert ähnlich wie bei NodeJS auf einem einzelnen Thread und gibt die Ausführung per async/await ab; schon mit nur einem Thread kann man einen ziemlich guten Durchsatz von Tausenden Requests pro Sekunde erreichen
Das große Problem ist jedoch, dass jede Art von CPU-Arbeit alle anderen Coroutines blockiert, sobald sie ausgeführt wird. Das führt zu allerlei schwer greifbaren Problemen und ruiniert die Requests-pro-Sekunde. Zum Beispiel sieht man in einer Coroutine scheinbar zufällige I/O-Timeouts, obwohl die eigentliche Ursache eine ganz andere Coroutine sein kann, die kurz die CPU belegt hat. Es ist außerdem sehr schwer zu beobachten, warum so etwas passiert. asyncio bietet die Funktion
asyncio.to_thread()[1], die dabei hilft, blockierende Arbeit aus dem Main-Thread herauszunehmen, aber wegen des GIL trennt sie CPU-lastige Arbeit nicht wirklich so ab, dass sie andere Coroutines nicht beeinflusst[1] https://docs.python.org/3/library/asyncio-task.html#asyncio....
Für alle, die es interessiert: GIL steht für Global Interpreter Lock
Gibt es dazu eine gute Übersicht über das größere Gesamtbild?
Ich freue mich endlich auf Benchmarks verschiedener Tools