Python ohne GIL: was der freie Threading-Modus im eigenen Code ändert
Der globale Lock fällt nicht überall weg, sondern nur in einem eigenen Build des Interpreters. Was dieser Bau leistet, wo er bremst und woran ihr in einer Stunde erkennt, ob euer Programm überhaupt der richtige Kandidat ist.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Die Schlagzeile lautet, der GIL sei weg, und beides stimmt nicht ganz
Der globale Interpreter-Lock sorgt dafür, dass immer nur ein Thread gleichzeitig Python-Bytecode ausführt. Für Programme, die auf Datenbank, Netz oder Dateien warten, hat er nie besonders gestört, weil die Standardbibliothek den Lock vor blockierenden Aufrufen freigibt. Wehgetan hat er dort, wo tatsächlich in Python gerechnet wird: Vier Threads über einer Rechenschleife sind mit Lock nicht schneller als einer, sie sind wegen des Wechselns sogar etwas langsamer.
Die verbreitete Antwort darauf war, Prozesse statt Threads zu nehmen. Das funktioniert, kostet aber jedes Mal dasselbe: Die Daten müssen zwischen den Prozessen serialisiert werden, jeder Prozess bringt seinen eigenen Interpreter und seinen eigenen Speicherbedarf mit, und alles, was sich schlecht übertragen lässt, etwa ein geladenes Modell oder ein großer Zwischenspeicher, liegt danach mehrfach im Arbeitsspeicher. Bei großen Datenmengen frisst genau dieser Umweg den Gewinn wieder auf.
Die Erwartung geht deshalb regelmäßig in die falsche Richtung. Wer den freien Bau installiert, weil er gelesen hat, dass Python jetzt ohne Lock läuft, und danach ein Programm misst, das ohnehin auf eine Datenbank wartet, sieht keinen Unterschied und schreibt die Sache ab. Wer umgekehrt eine rechenlastige Auswertung umstellt und nicht prüft, ob der Lock zur Laufzeit tatsächlich aus ist, misst am Ende einen Bau, der sich stillschweigend wieder eingeschaltet hat.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Ein Aufruf, der die Lage in zwei Zeilen klärt
Der folgende Aufruf installiert den freien Bau, startet ihn mit ausdrücklich abgeschaltetem Lock und lässt sich sowohl vom Bau als auch vom laufenden Prozess bestätigen, dass wirklich ohne Lock gearbeitet wird. Jeder Bestandteil beantwortet eine eigene Frage.
Der Aufbau
uv python install 3.14t && python3.14t -X gil=0 -c "import sys, sysconfig; print(sysconfig.get_config_var('Py_GIL_DISABLED'), sys._is_gil_enabled())" - 01 Der Bau muss erst beschafft werden
uv python install 3.14tDer freie Modus ist kein Schalter an einer vorhandenen Installation, sondern ein eigener Bau des Interpreters. Die offiziellen Installationsprogramme für Windows und macOS bieten ihn als zusätzliche Auswahl an, Werkzeuge wie uv holen ihn über die Kennung mit angehängtem t. Wer aus Quellen baut, braucht die Konfiguration mit --disable-gil.
- 02 Das t im Namen ist die eigentliche Aussage
python3.14tDer freie Bau liegt als eigene ausführbare Datei neben der gewöhnlichen und stört sie nicht. Er hat auch eine eigene Kennung für Pakete, weshalb Bibliotheken mit kompiliertem Anteil eigens dafür gebaute Dateien brauchen. Für reinen Python-Code ändert sich am Paket nichts.
- 03 Der Schalter für den laufenden Prozess
-X gil=0Auch der freie Bau kann mit eingeschaltetem Lock laufen. Mit diesem Schalter erzwingst du das Gegenteil, mit gil=1 den umgekehrten Fall, und dieselbe Wirkung hat die Umgebungsvariable PYTHON_GIL. Genau das macht saubere Vergleichsmessungen möglich, weil derselbe Interpreter beide Zustände einnehmen kann.
- 04 Die Frage an den Bau
sysconfig.get_config_var('Py_GIL_DISABLED')Diese Abfrage sagt, ob der Interpreter überhaupt für den freien Modus gebaut wurde. Die Dokumentation empfiehlt genau diesen Weg für alle Entscheidungen, die von der Bauart abhängen, etwa in Installationsroutinen oder in Erweiterungen. Sie sagt nichts darüber aus, ob der Lock im Moment tatsächlich aus ist.
- 05 Die Frage an den laufenden Prozess
sys._is_gil_enabled()Das ist die Antwort, auf die es ankommt. Sie berücksichtigt sowohl den Schalter beim Start als auch den Fall, dass eine importierte Erweiterung den Lock nachträglich wieder eingeschaltet hat. Wenn hier nach dem Import aller Abhängigkeiten ein wahrer Wert steht, misst du gerade keinen freien Betrieb.
Wenn es nicht funktioniert
Das siehst du
Das Programm läuft im freien Bau, die Laufzeit bleibt aber genau dieselbe, und die zusätzlichen Kerne bleiben unbeschäftigt.
Warum
Beim Import einer Erweiterung mit kompiliertem Anteil, die nicht ausdrücklich als geeignet gekennzeichnet ist, schaltet der Interpreter den Lock von sich aus wieder ein. Er gibt dabei eine Warnung aus, die zwischen den übrigen Startmeldungen leicht untergeht, und läuft danach wie ein gewöhnlicher Bau weiter.
Was hilft
Frag nach dem Import aller Abhängigkeiten sys._is_gil_enabled() ab und lass diese Prüfung im Testlauf mitlaufen. Um den Verursacher zu finden, importierst du die Abhängigkeiten einzeln und siehst zu, an welcher Stelle die Warnung erscheint. Danach hilft nur ein Update dieser Bibliothek oder ein Austausch. Den Lock mit dem Schalter zu erzwingen ist keine Lösung, weil die Erweiterung dann ohne den Schutz läuft, für den sie geschrieben wurde.
Das siehst du
Die Installation der Abhängigkeiten dauert plötzlich sehr lange, es laufen Compiler-Ausgaben durch, und auf einem Rechner ohne Build-Werkzeuge bricht sie ganz ab.
Warum
Für den freien Bau gibt es eine eigene Paketkennung. Wo keine passende fertige Datei vorliegt, versucht die Installation, das Paket aus den Quellen zu übersetzen. Das gelingt oft, dauert aber lange und setzt eine vollständige Build-Umgebung samt passender Systembibliotheken voraus.
Was hilft
Prüf die Abhängigkeitsliste vorher gegen den öffentlichen Tracker zur Unterstützung des freien Baus. Der wissenschaftliche Kern mit NumPy, SciPy und den großen Frameworks für maschinelles Lernen liefert passende Dateien inzwischen mit. Für einzelne Nachzügler ist die ehrlichste Antwort, den Umstieg für dieses Projekt noch zu vertagen.
Das siehst du
Ein Teil der Anwendung ist im freien Bau messbar langsamer geworden, obwohl an der Nebenläufigkeit nichts geändert wurde.
Warum
Der freie Bau braucht zusätzliche Buchführung, um ohne den einen großen Lock sicher zu bleiben. Auf der offiziellen Messreihe liegt der Aufschlag je nach System zwischen etwa einem Prozent und acht Prozent. Bei Code, der ohnehin nur einen Pfad hat, ist dieser Aufschlag der gesamte Effekt.
Was hilft
Stell nicht die ganze Anwendung um, sondern nur den Teil, der tatsächlich mehrere Kerne auslasten kann. In vielen Fällen ist das ein eigener Dienst oder ein eigener Arbeitsprozess. Miss beide Zustände mit demselben Interpreter über den Schalter, dann vergleichst du wirklich den Lock und nicht zwei verschiedene Installationen.
Das siehst du
Unter Last kommen falsche Zahlen heraus, Einträge fehlen in einer gemeinsam benutzten Sammlung, oder das Programm bricht sporadisch ab.
Warum
Der Lock hat vorher dafür gesorgt, dass viele Zugriffe in der Praxis nicht unterbrochen wurden, obwohl das nie zugesichert war. Ohne ihn treten genau die Race Conditions auf, die im Code seit Jahren stehen. Die Dokumentation nennt dazu ausdrücklich zwei Fallen: denselben Iterator aus mehreren Threads zu benutzen und sich auf die internen Sperren eingebauter Typen zu verlassen.
Was hilft
Sichere gemeinsam benutzten veränderlichen Zustand ausdrücklich mit threading.Lock ab, statt auf implizite Unteilbarkeit zu bauen. Gib jedem Thread seinen eigenen Iterator und führe die Ergebnisse danach zusammen. Für die Fehlersuche helfen Lastläufe mit deutlich mehr Threads als Kernen, weil sich damit die Zeitfenster vergrößern, in denen die Rennen sichtbar werden.
Das siehst du
Alles läuft korrekt und der Lock ist nachweislich aus, trotzdem bleibt der Tempogewinn weit hinter der Zahl der Kerne zurück.
Warum
Entweder liegt ein gemeinsamer Engpass davor, etwa eine Sperre um eine gemeinsame Datenstruktur oder eine Datenbankverbindung, oder die Arbeit lässt sich nicht gleichmäßig verteilen. Häufig ist auch der Speicherzugriff der begrenzende Faktor und nicht die Rechenzeit.
Was hilft
Miss die Skalierung ausdrücklich mit zwei, vier und acht Threads, statt nur einmal mit allen. Ein Verlauf, der schon bei zwei Threads abflacht, zeigt auf eine gemeinsame Sperre. Prüf danach, ob die Arbeit in größere unabhängige Blöcke zerlegt werden kann, denn viele kleine Aufgaben mit gemeinsamem Zustand kosten mehr Verwaltung als sie an Parallelität bringen.
Fünf Fragen, und du weißt, ob der freie Bau etwas bringt
- 01 Liegt die Zeit in Python-Code oder im Warten? Nur im ersten Fall ändert sich etwas.
- 02 Rechnet schon eine Bibliothek mit kompiliertem Kern? Dann ist der Lock dort ohnehin frei.
- 03 Wie viele Kerne stehen der Anwendung überhaupt zur Verfügung?
- 04 Gibt es für alle Abhängigkeiten Pakete für diesen Bau? Sonst wird aus Sekunden ein Bauvorgang.
- 05 Teilen mehrere Threads veränderlichen Zustand? Dann fehlt euch vorher eine Sperre.
Was du danach an einem Nachmittag entscheidest
Die Frage lässt sich messen statt diskutieren. Sechs Schritte reichen, um für ein konkretes Programm zu klären, ob der freie Bau etwas bringt, und wenn ja, was er an anderer Stelle kostet.
Zuerst prüfen, ob überhaupt in Python gerechnet wird
Nimm ein Profil auf und sieh nach, wo die Zeit liegt. Steckt sie im Warten auf Netz und Datenbank, ändert der freie Bau nichts, denn dort war der Lock ohnehin freigegeben. Steckt sie in einer Rechenbibliothek mit kompiliertem Kern, gilt dasselbe, weil solche Bibliotheken den Lock während der Rechenarbeit von sich aus abgeben.
Den Bau neben der bestehenden Installation halten
Der freie Bau ist ein eigener Interpreter mit eigenem Namen und eigenen Paketen. Er verdrängt die vorhandene Installation nicht, sondern steht daneben. Damit lässt sich derselbe Code in beiden Umgebungen messen, ohne dass die Arbeitsumgebung des Teams angefasst werden muss.
Nach dem Start prüfen, ob der Lock wirklich aus ist
Der freie Bau kann den Lock zur Laufzeit wieder einschalten, wenn eine importierte Erweiterung nicht als geeignet gekennzeichnet ist. Dabei erscheint eine Warnung, die in einem ausführlichen Log leicht untergeht. Frag den laufenden Prozess deshalb ausdrücklich, statt dich auf den Namen des Interpreters zu verlassen.
Die Abhängigkeiten vorab gegen den Tracker halten
Reiner Python-Code läuft ohne Änderung. Alles mit kompiliertem Anteil braucht Pakete, die für diesen Bau gebaut wurden. Der große wissenschaftliche Kern liefert sie inzwischen, im langen Rest der Abhängigkeitsliste findet sich aber fast immer eine Bibliothek, die es noch nicht tut.
Den Aufschlag bei Einzelthread-Code einplanen
Der freie Bau führt Python-Code langsamer aus als der gewöhnliche. Auf der offiziellen Messreihe liegt der Aufschlag je nach System und Last zwischen etwa einem und acht Prozent. Bei vier oder acht nutzbaren Kernen ist das nebensächlich, bei einem Programm mit einem einzigen Rechenpfad ist es ein reiner Verlust.
Vorhandene Threads auf falsche Annahmen durchsehen
Code, der bisher unter dem Lock lief, verlässt sich oft unbewusst darauf, dass einzelne Operationen nicht unterbrochen werden. Ohne Lock stimmt das nicht mehr zuverlässig. Bevor ihr umstellt, gehört jeder gemeinsam benutzte Zustand einmal durchgesehen und mit einer ausdrücklichen Sperre versehen.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Warum der Lock so lange geblieben ist
Der globale Lock war nie ein Versehen, sondern ein Tauschgeschäft. Er macht die Speicherverwaltung des Interpreters einfach und schnell, weil die Zähler, über die Python entscheidet, wann ein Objekt weg darf, ohne aufwendige Absicherung hochgezählt werden dürfen. Er macht außerdem das Schreiben von Erweiterungen in C einfach, weil solcher Code sich darauf verlassen kann, dass ihm niemand dazwischenfunkt. Der Preis war, dass reiner Python-Code nie mehr als einen Kern gleichzeitig belegen konnte.
Frühere Anläufe, den Lock zu entfernen, sind an derselben Stelle gescheitert: Die Absicherung kostete bei Einzelthread-Code mehr, als die Parallelität einbrachte. Der Vorschlag, der es geschafft hat, ist in PEP 703 beschrieben und geht das Problem mit einer anderen Speicherverwaltung an. Er wurde 2023 angenommen, allerdings ausdrücklich mit der Auflage, dass die Umstellung schrittweise und ohne Bruch für das bestehende Ökosystem geschieht.
Deshalb gibt es zwei Bauarten nebeneinander und nicht einen Schalter. Ein Paket mit kompiliertem Anteil, das für den gewöhnlichen Bau übersetzt wurde, kann im freien Bau nicht einfach mitlaufen, es braucht eine eigene Fassung. Genau diese Trennung ist der Grund für den größten Teil der praktischen Reibung, und sie ist auch der Grund, warum die Umstellung sich über mehrere Jahre zieht.
Was der Stand seit Version 3.14 tatsächlich bedeutet
In Python 3.13 war der freie Bau vorhanden, aber als Experiment gekennzeichnet. Mit PEP 779, das der Steering Council im Juni 2025 angenommen hat, ist er in die zweite Phase gewechselt: offiziell unterstützt, aber weiterhin optional. Der Unterschied ist mehr als eine Formulierung. Er bedeutet, dass Fehler in dieser Bauart wie gewöhnliche Fehler behandelt werden, dass die Schnittstelle für Erweiterungen als stabil genug gilt und dass Bibliotheken sich darauf einrichten können, ohne bei jeder Nebenversion mit Umbrüchen zu rechnen.
Der Vorschlag nennt dabei auch Grenzwerte, an denen die Phase gemessen wurde, unter anderem einen Aufschlag von höchstens etwa fünfzehn Prozent bei Einzelthread-Code und höchstens etwa zwanzig Prozent mehr Speicherbedarf. Die tatsächlichen Werte liegen inzwischen darunter. Für die Praxis heißt das: Der freie Bau ist kein Prototyp mehr, er ist aber auch nicht der Normalfall, und beides ist beabsichtigt.
Was ausdrücklich nicht entschieden ist, ist die dritte Phase, also der Zeitpunkt, an dem der freie Bau zum Standard würde. Der Vorschlag lässt diese Kriterien offen und macht sie von der Erfahrung aus dem echten Einsatz abhängig. Wer heute plant, sollte deshalb davon ausgehen, dass beide Bauarten noch mehrere Jahre nebeneinander existieren, und die eigene Werkzeugkette darauf einrichten, statt auf einen Stichtag zu warten.
Welche Lasten wirklich profitieren
Der klare Gewinnfall ist Rechenarbeit in Python selbst, die sich in unabhängige Teile zerlegen lässt und auf denselben Daten arbeitet: das Auswerten vieler Datensätze nach eigener Logik, Simulationen, Verarbeitungsschritte über Bäume oder Graphen, das Zerlegen großer Textmengen. Bisher blieb dafür nur der Umweg über Prozesse, und genau dessen Kosten fallen jetzt weg. Besonders deutlich wird der Unterschied, wenn im Speicher etwas Großes liegt, das sich schlecht kopieren lässt.
Kein Gewinn ist zu erwarten, wo auf etwas gewartet wird. Für Datenbankabfragen, Netzaufrufe und Dateizugriffe geben die entsprechenden Funktionen den Lock ohnehin frei, weshalb Threads dort schon immer nebeneinander laufen konnten. Ebenso wenig ändert sich, wenn die Rechenarbeit bereits in einer Bibliothek mit kompiliertem Kern stattfindet, denn diese Bibliotheken geben den Lock während der Rechnung frei und nutzen intern oft schon mehrere Kerne.
Dazwischen liegt der Fall, der am häufigsten vorkommt und am schwersten einzuschätzen ist: eine Auswertung, die zwar auf einer Rechenbibliothek aufsetzt, den größten Teil der Zeit aber in Schleifen aus eigenem Python-Code verbringt, die zwischen den Aufrufen liegen. Hier lohnt die Messung wirklich, weil das Ergebnis von Fall zu Fall unterschiedlich ausfällt und sich nicht aus der Beschreibung der Anwendung ableiten lässt.
Die Alternativen, die weiterhin richtig sein können
Getrennte Prozesse bleiben die naheliegende Antwort, wenn die Arbeitspakete groß sind und wenig Daten hin und her müssen, oder wenn ihr Abhängigkeiten habt, die es für den freien Bau noch nicht gibt. Der Ansatz funktioniert seit Jahren, ist gut verstanden und hat den angenehmen Nebeneffekt, dass ein Absturz in einem Arbeitsprozess nicht die ganze Anwendung mitnimmt.
Seit Python 3.14 gibt es daneben eine dritte Möglichkeit, die selten erwähnt wird: mehrere Interpreter innerhalb eines Prozesses, in der Standardbibliothek unter concurrent.interpreters erreichbar und in PEP 734 beschrieben. Jeder Interpreter hat seinen eigenen Zustand und damit seinen eigenen Lock, die Verständigung läuft über Warteschlangen. Das liegt zwischen Threads und Prozessen: weniger Aufwand als ein eigener Prozess, aber ohne den gemeinsamen Speicher, den Threads bieten.
Und schließlich bleibt der Weg, den rechenintensiven Teil aus Python herauszunehmen, also in eine Erweiterung in C, C++ oder Rust zu verlagern. Das ist mehr Arbeit, bringt aber oft einen Faktor, den keine Parallelisierung erreicht, und es funktioniert unabhängig davon, welchen Interpreter-Bau ihr einsetzt. Für eine einzelne heiße Schleife ist das häufig die kleinere Änderung, als sie zunächst aussieht.
Dazu passende Kurse
Wenn im Team unterschiedliche Vorstellungen davon bestehen, was Threads in Python leisten, klären Python-Kurse mit Blick auf Threads und Rechenlast das an ausführbarem Code.
Wie sicher bist du beim Thema wirklich?
Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.
Wo du genau das übst
Sehr umfangreicher Überblick über Zusammenhänge und Anwendungsbereiche. Sehr zu empfehlen.
Effektives, gemeinsames Erarbeiten der Aufgaben mit vielen praktischen Tipps.
Sehr guter Trainer und gute Aufbereitung der Inhalte und auch eingehen auf Fragen der Beteiligten.
Häufige Fragen
Muss ich meinen Code ändern, damit er im freien Bau läuft?
Ist der freie Bau schon produktiv einsetzbar?
Wird der freie Bau irgendwann der Standard?
Was ist mit asyncio, brauche ich das jetzt nicht mehr?
Wie messen wir den Unterschied verlässlich?
Deine Ansprechpartner
Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Hilft dir, aus dem Programmierung-Programm den passenden Kurs für deinen Stand zu finden.
Norbert Jansen
Beratung & Inhouse
Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.
Nebenläufigkeit an eigenem Code prüfen
Im Python-Aufbaukurs bei cmt gehst du Threads, Prozesse und gemeinsam benutzten Zustand an Beispielen durch, die deinem eigenen Code nahekommen, und siehst, wo eine Sperre wirklich hingehört.