1 Punkte von GN⁺ 2024-03-12 | 1 Kommentare | Auf WhatsApp teilen
  • CPython PR #116338 hat die Änderung „Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 in python:main gemergt, 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() und drop_gil() frühzeitig zurückkehren
  • Erste Prüfungen zeigten, dass mit PYTHON_GIL=0 einige Tests und kleine Programme ohne Threads normal liefen und sehr grundlegende Thread-Programme manchmal funktionierten, die vollständige Test-Suite jedoch schnell in test_asyncio abstürzte
  • Im Review wurden Tests für PYTHON_GIL, Dokumentation, die Option -X gil sowie die Abbildung in sys.flags ergänzt; außerdem wurde die Verarbeitung so korrigiert, dass PYTHON_GIL=1 die 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
  • colesbury hat sie am 11. März 2024 in python:main gemergt
  • 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() und drop_gil() frühzeitig zurück
  • Während des Reviews wurde auch ein Commit ergänzt, der enable_gil bei PYTHON_GIL=1 korrekt setzt

Tests und aktuelle Einschränkungen

  • Mit der Einstellung PYTHON_GIL=0 wurden 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_asyncio festgehalten
  • Mit dem Befehl !buildbot nogil wurden mehrfach Builder-Tests rund um NoGIL eingeplant
    • x86-64 MacOS Intel ASAN NoGIL PR
    • x86-64 MacOS Intel NoGIL PR
    • ARM64 MacOS M1 Refleaks NoGIL PR
    • ARM64 MacOS M1 NoGIL PR
    • AMD64 Ubuntu NoGIL Refleaks PR
    • AMD64 Ubuntu NoGIL PR
    • AMD64 Windows Server 2022 NoGIL PR

Im Review ergänzter Umfang

  • corona10 schlug vor, in Lib/test/test_cmd_line.py Tests für die Umgebungsvariable zu ergänzen
  • Danach wurden die folgenden Commits hinzugefügt
    • Add test for PYTHON_GIL in test_cmd_line
    • Set enable_gil properly when PYTHON_GIL=1
    • Don't add 'enable_gil' to test_embed in normal builds
  • colesbury hielt 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-gil bereits dokumentiert ist
    • Für die Dokumentation hielt er fest, dass die Variable nur in free-threaded Builds verfügbar ist, 0 die Deaktivierung des GIL erzwingt, 1 die Aktivierung des GIL erzwingt und dass dies neu in Python 3.13 ist
  • Anschließend wurde der Commit Document PYTHON_GIL environment variable hinzugefü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=0 auf eine Variante erweitert, die auch -X gil=0 einschließt
  • Die zusätzlichen Commits enthielten unter anderem Folgendes
    • Add -X gil option, add to sys.flags, modify test to cover env var… and option
    • Fix link to -X gil
    • Fix PYTHON_GIL versionchanged line
    • Clarify test_flags in normal builds
  • ericsnowcurrently, erlend-aasland, corona10 und colesbury haben die Änderung freigegeben
  • Der Merge-Commit ist 2731913; nach dem Merge reagierte vstinner auf die Änderung mit den Worten, sie sei „interessant und sehr beängstigend“

Folgearbeiten

  • Als Folge-Issues wurden zwei Aufgaben getrennt erfasst
    • #116322: den GIL wieder aktivieren, wenn inkompatible Erweiterungen geladen werden
    • #116329: den GIL standardmäßig deaktivieren
  • 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

 
GN⁺ 2024-03-12
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

  • 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

  • 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

    • CPython könnte künftig vielleicht Eigentumsübertragungen implementieren, aber das ist etwas kniffliger
      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.SimpleQueue legt, 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 wird
      Der 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?

    • Früher hat man wegen des GIL faktisch kaum multithreaded Python geschrieben. Threads wurden vor allem genutzt, um mehrere Aufgaben zu bearbeiten, die jeweils unabhängig bei Ein-/Ausgabe blockieren konnten; das ist natürlich verbreitet und nützlich, half aber nicht bei der Performance CPU-lastigen Python-Codes
      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....
    • Wenn irgendein Paket vom GIL abhängt, bleibt der GIL aktiviert. Pakete gehen also nicht kaputt
  • 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