Nebenläufigkeit: Threads, Engpass, Little’s und Amdahl’s Law und der GIL in Python
Track Konzepte · Nebenläufigkeit und Last · ca. 45 Min. (plus 10 Min. optionale Zusatzübung)
Worum es geht
Du kennst den Event Loop aus JavaScript. Hier lernst du, dass er kein JS-Trick ist, sondern ein allgemeines Muster, und dass Python zusätzlich Threads, Prozesse und den GIL (Global Interpreter Lock) kennt. Genau dort liegt eine deiner bekannten Lücken: Wann hilft welches Werkzeug, und wann hilft keines?
Am Ende dieses Teils kannst du:
- erklären, warum mehr Threads nicht automatisch mehr Durchsatz bringen, und den Engpass benennen (Little’s Law, Amdahl’s Law, Auslastung und Warteschlange),
- für Python entscheiden:
asyncio, Threads oder Prozesse, und begründen, was der GIL daran ändert.
Event Loop mit Blockierung, Backpressure (Bounded Queue) und Cancellation durch eine Kette folgen in Teil 2.
Echte Threads laufen im Browser (Pyodide) nicht parallel. Darum simulieren wir Zeit als Zähler. Messungen mit echten Threads und Prozessen stehen als lokal ausgeführte Ausgabe im Text, und es gibt eine lokale Zusatzübung in lernlabor/.
Plane ehrlich 45 Minuten ein: etwa 20 Minuten Lesen, 25 Minuten Übungen (drei Stück, dazu optional die lokale Zusatzübung mit 10 Minuten). Eine gute Pause ist nach Übung 2.
Locks, Race Conditions und Deadlocks gibt es schon: Race Conditions in Lektion 01, Deadlock in Lektion 03, Retry, Timeout und Rate Limiting in Lektion 07 Teil 1 und Teil 2. Das wiederholen wir hier nicht.
Von JS/TS her gedacht
| Konzept | JS/TS | Python |
|---|---|---|
| Ein Thread, viele wartende Aufgaben | Event Loop, async/await, Promise |
asyncio Event Loop, async/await, Coroutine |
| Echte Parallelität | worker_threads, Web Worker |
multiprocessing, ProcessPoolExecutor (Prozesse) |
| Threads mit gemeinsamem Speicher | selten nötig | threading, ThreadPoolExecutor (vom GIL begrenzt) |
In JS gibt es den GIL-Effekt nicht in dieser Form: Ein Worker hat einen eigenen Speicher, und async läuft immer in einem Thread. In Python musst du die Wahl zwischen den drei Zeilen der Tabelle bewusst treffen, und diese Wahl ist der Kern dieses Teils.
Konzept
Schritt 1: Wer läuft wo, und wer teilt sich was?
| Prozess (process) | Thread | Coroutine | |
|---|---|---|---|
| Speicher | eigener Adressraum | gemeinsam mit den anderen Threads | gemeinsam, alles im selben Thread |
| Wer wechselt? | Betriebssystem | Betriebssystem (preemptiv, es unterbricht dich jederzeit) | du selbst am await (kooperativ) |
| Kosten pro Stück | hoch | mittel (eigener Stack) | gering |
| Austausch | Nachrichten, Pipes, Serialisierung | direkt über gemeinsame Variablen (daher Locks) | direkt, aber ohne Unterbrechung zwischen zwei await |
Zwei Begriffe werden oft verwechselt. Concurrency heißt: mehrere Aufgaben sind gleichzeitig in Arbeit, auch wenn nur eine gerade rechnet (ein Koch, der Wasser aufsetzt und währenddessen Gemüse schneidet). Parallelism heißt: mehrere Aufgaben rechnen wirklich gleichzeitig auf mehreren Kernen (zwei Köche). Dein Node-Server ist nebenläufig, aber nicht parallel.
Es gibt vier gängige Modelle der Nebenläufigkeit:
| Modell | Idee | Gefahr |
|---|---|---|
| Threads und Locks | gemeinsamer Speicher, Locks schützen ihn | Race Condition, Deadlock |
| Actors | jeder Actor besitzt seinen Zustand, Kommunikation nur über Nachrichten (z. B. Erlang) | Mailbox läuft voll, Reihenfolge über Actors hinweg |
| CSP und Channels | Aufgaben reden über Kanäle (z. B. Go) | Kanal blockiert, Deadlock |
async/await |
Coroutinen, die am await Kontrolle abgeben |
ein blockierender Aufruf hält alle an |
Die Quelle nennt async/await eine Zustandsmaschine (state machine). Das siehst du hier wörtlich. Eine Coroutine ist ein Objekt, das an jedem await anhält und später dort weitermacht. Wir bauen den kleinsten denkbaren Event Loop selbst: Er schiebt reihum jede Coroutine bis zum nächsten await weiter.
Ausgabe:
A Schritt 1: Lager prüfen
-> wartet auf Lager
B Schritt 1: Lager prüfen
-> wartet auf Lager
A Schritt 2: Zahlung
-> wartet auf Zahlung
B Schritt 2: Zahlung
-> wartet auf Zahlung
A Schritt 3: fertig
-> Ergebnis: A ok
B Schritt 3: fertig
-> Ergebnis: B ok
So liest du das: coro.send(None) startet die Coroutine oder setzt sie fort, bis sie am nächsten await anhält. Der Wert, den Pause.__await__ per yield hinausgibt, kommt als Rückgabe von send an. Ist die Coroutine fertig, löst send eine StopIteration aus, und in ende.value steckt ihr return-Wert. Die while-Schleife ist unser Mini-Event-Loop.
A und B laufen abwechselnd, obwohl nur ein Thread im Spiel ist. Ein echter Event Loop macht dasselbe, nur dass er eine Coroutine erst dann weiterschiebt, wenn ihr Warten (Netzwerk, Timer) beendet ist. Das ist dasselbe Muster wie in JS. Daraus folgt die wichtigste Regel: Zwischen zwei await wirst du nie unterbrochen, aber auch kein anderer kommt dran. Ein langer Rechenschritt ohne await hält alle anderen an.
Zum Speichermodell nur kurz (allgemeines Fachwissen): Ohne Synchronisation garantiert eine Sprache nicht, dass ein Thread die Schreibzugriffe eines anderen sofort und in derselben Reihenfolge sieht (Sichtbarkeit, happens-before). Locks, Queues und atomare Operationen schaffen diese Ordnung. Das ist ein weiterer Grund, warum “es hat im Test funktioniert” bei Threads nichts beweist. Details zu Locks stehen in Lektion 01.
Schritt 2: Warum mehr Threads nicht automatisch mehr Durchsatz bringen
Little’s Law verbindet drei Größen eines stabilen Systems: Im Mittel sind L = λ · W Aufträge gleichzeitig im System. λ ist die Ankunftsrate (Aufträge pro Sekunde), W die mittlere Zeit eines Auftrags im System, L die Zahl gleichzeitiger Aufträge. Beispiel: 20 Anfragen pro Sekunde, jede 0,5 Sekunden im System, ergibt L = 20 · 0,5 = 10. Du brauchst also mindestens 10 gleichzeitig arbeitende Worker (Threads, Verbindungen, Coroutinen). Weniger, und die Warteschlange wächst.
Amdahl’s Law sagt, wie viel schneller ein Auftrag mit n Workern wird, wenn nur ein Anteil p parallel läuft: S(n) = 1 / ((1 - p) + p / n). Der serielle Rest 1 - p setzt eine Obergrenze 1 / (1 - p), egal wie groß n wird.
Ausgabe:
Little: 20 Anfragen/s, je 0,5 s -> 10.0 gleichzeitig in Arbeit
Amdahl p=0.5, n=1: 1.000
Amdahl p=0.5, n=2: 1.333
Amdahl p=0.5, n=4: 1.600
Amdahl p=0.5, n=8: 1.778
Amdahl p=0.5, n=16: 1.882
Amdahl p=0.5, n=1000: 1.998
Grenze 1/(1-p) = 2.0
Bei halb seriellem Programm bringen 1000 Worker nicht einmal das Doppelte. In der Praxis ist der serielle Teil oft eine gemeinsame Ressource: ein Lock, der GIL, eine einzelne Datenbankverbindung, ein Dateihandle. Das modellieren wir mit einem kleinen Pool-Simulator. Jeder Auftrag besteht aus wartezeit Ticks paralleler Arbeit (z. B. auf das Netzwerk warten) und danach seriell Ticks an einer gemeinsamen Ressource, die nur ein Worker zur Zeit nutzen kann. n Worker machen das immer wieder. Die Funktion liefert, wie viele Aufträge fertig werden, wie ausgelastet die Ressource ist und wie viele Worker im Mittel vor ihr warten.
Ausgabe:
n= 1 Aufträge=125 Ressource ausgelastet=25% Ø wartende=0.0
n= 2 Aufträge=249 Ressource ausgelastet=50% Ø wartende=0.0
n= 4 Aufträge=497 Ressource ausgelastet=99% Ø wartende=0.0
n= 8 Aufträge=497 Ressource ausgelastet=99% Ø wartende=4.0
n=16 Aufträge=497 Ressource ausgelastet=99% Ø wartende=12.0
Lies das so: Ein Auftrag dauert 6 + 2 = 8 Ticks, ein Worker schafft 125 Aufträge in 1000 Ticks. Zwei Worker schaffen doppelt so viele, vier fast vier Mal so viele. Dann ist die Ressource zu fast 100 Prozent ausgelastet, und mehr Worker bringen nichts mehr: 8 und 16 Worker schaffen dieselben 497 Aufträge.
Die Formel dahinter: Durchsatz pro Tick = min(n / (wartezeit + seriell), 1 / seriell). Der erste Wert ist die Obergrenze der Worker, der zweite die Obergrenze der Ressource. Die Ressource braucht 2 Ticks pro Auftrag, also schafft sie höchstens 1/2 Auftrag pro Tick, das sind 500 pro 1000 Ticks. Ab n = 4 greift diese Grenze (4 / 8 = 1/2).
Schritt 3: Woran erkennst du den Engpass?
Drei Signale, in dieser Reihenfolge prüfen (allgemeines Fachwissen):
- Auslastung (utilization) der Ressource nahe 100 Prozent, während der Durchsatz nicht mehr steigt. Das ist der Engpass.
- Warteschlange vor der Ressource wächst mit
n(Spalte “Ø wartende” oben: 0, 4, 12 für n = 4, 8, 16). Wartende Worker tun nichts, sie kosten nur Speicher. - Latenz pro Auftrag steigt, obwohl die Arbeit selbst gleich bleibt. Die Zeit geht ins Warten.
Little’s Law gilt auch für das ganze System: Von den n Workern ist jeder entweder in der Warte-Phase, an der Ressource oder in deren Schlange. Bei n = 16: Durchsatz 0,5 pro Tick, davon 0,5 · 6 = 3 Worker in der Warte-Phase, 0,5 · 2 = 1 an der Ressource, bleiben 16 - 3 - 1 = 12 in der Schlange. Genau der Wert aus der Tabelle.
Was tun? Nicht mehr Worker, sondern die Ressource entlasten: den seriellen Teil verkürzen, die Ressource vervielfachen (mehr Datenbankverbindungen, mehr Prozesse mit je eigenem GIL) oder die Arbeit anders aufteilen. Das ist die Antwort auf den ersten Prüfstein, du übst sie in Übung 1 und 2. ### Schritt 4: Python und der GIL
Der GIL (Global Interpreter Lock) ist ein Lock im CPython-Interpreter: Es führt immer nur ein Thread Python-Bytecode zugleich aus. Beim Warten auf I/O (Netzwerk, Platte, time.sleep) gibt ein Thread den GIL frei, andere dürfen rechnen. Daraus folgt:
- Threads helfen bei I/O-bound Arbeit (die meiste Zeit wird gewartet).
- Threads helfen bei CPU-bound Arbeit in reinem Python nicht (die Schleifen stehen sich beim GIL gegenseitig im Weg). Dafür gibt es Prozesse, jeder mit eigenem Interpreter und eigenem GIL.
- Der GIL macht deinen Code nicht thread-sicher:
zaehler += 1besteht aus mehreren Schritten, zwischen denen ein anderer Thread dazwischenfunken kann (Lost Update wie in Lektion 01, allgemeines Fachwissen). - C-Erweiterungen können den GIL freigeben und dann wirklich parallel rechnen (je Bibliothek prüfen, bitte prüfen). Seit Python 3.13 gibt es außerdem einen optionalen free-threaded Build ohne GIL (PEP 703, Status bitte prüfen).
sys._is_gil_enabled()sagt dir, ob er aktiv ist.
Die Entscheidungshilfe:
| Arbeit | Wartet worauf? | Typische Wahl |
|---|---|---|
| sehr viele gleichzeitige Verbindungen, es gibt eine async-Bibliothek | Netzwerk | asyncio |
| einige Dutzend Aufrufe, Bibliothek nur blockierend | Netzwerk, Platte | ThreadPoolExecutor oder asyncio.to_thread (siehe Teil 2) |
| Rechnen in reinem Python auf mehreren Kernen | CPU | ProcessPoolExecutor (multiprocessing) |
| wenig Arbeit, kein Zeitdruck | egal | sequenziell, ohne Nebenläufigkeit |
Wieder: keine Faustregel ohne Messung. Ein Beispiel aus dem lernlabor (Skript lernlabor/uebung/konzepte/konzepte_19_gil_messung.py, lokal ausgeführt mit Python 3.13.9, sys._is_gil_enabled() ist True). Es führt je vier Aufträge sequenziell, in Threads und in Prozessen aus. “cpu” ist eine reine Python-Schleife, “io” ist vier Mal time.sleep(0.3):
Python 3.13.9, GIL aktiv: True
cpu sequenziell: 0.37 s threads: 0.34 s prozesse: 0.13 s
io sequenziell: 1.23 s threads: 0.31 s prozesse: 0.37 s
Die Zahlen hängen von Rechner und Last ab. Die Muster nicht: CPU-Arbeit wird mit Threads kaum schneller (0,37 zu 0,34 s), mit Prozessen deutlich (0,13 s). I/O-Warten wird mit Threads fast so schnell wie das längste Warten (0,31 s statt 1,23 s). Prozesse kosten beim Start und Datenaustausch extra.
Zusatzübung (lokal): selbst messen (optional, 10 Min.)
So gehst du vor:
- Öffne
lernlabor/uebung/konzepte/konzepte_19_gil_messung.py. Die ZeileENTSCHEIDUNG = {"cpu": None, "io": None}ist deine Aufgabe, der Kopf der Datei erklärt sie. - Starte das Skript im Terminal (Voraussetzung:
uvist installiert, das Kommandouv --versionzeigt es dir). Es misst und beschwert sich, dass noch nichts eingetragen ist.
cd lernlabor && uv run python uebung/konzepte/konzepte_19_gil_messung.py- Trage für “cpu” und “io” jeweils
"sequenziell","threads"oder"prozesse"ein, welche Variante dort am schnellsten sein sollte, und starte noch einmal. Der Selbsttest akzeptiert deine Wahl, wenn ihre Zeit höchstens 50 Prozent über der schnellsten Variante liegt. - Zum Weiterdenken: Ändere in
lastendie Zahl3_000_000auf300_000und starte erneut. Gewinnen Prozesse bei so wenig Arbeit noch? Was kosten Prozesse beim Start?
Die Zahlen aus dem Text oben kennst du schon. Entscheidend ist, dass du auf deinem Rechner das Muster selbst siehst und begründest, warum es so ist.
Frage pro Arbeitslast: Wird gerechnet oder gewartet? Wann gibt ein Thread den GIL frei, und wann nicht?
ENTSCHEIDUNG = {"cpu": "prozesse", "io": "threads"}Die Rechenschleife hält den GIL, Threads kommen sich in die Quere, nur Prozesse (je ein eigener GIL) laufen auf mehreren Kernen. Beim Warten gibt der Thread den GIL frei, darum reichen Threads für I/O und sind billiger als Prozesse.
Falle
- Mehr Threads als Lösung für Langsamkeit. Ohne den Engpass zu messen (Auslastung, Warteschlange) verschiebst du das Warten nur. Hinter 100 Threads steht oft eine Datenbank mit 10 Verbindungen.
- GIL gleichsetzen mit “Threads sind sicher”. Er schützt den Interpreter, nicht deine Invarianten.
- Prozesse für alles nehmen. Sie lösen CPU-Arbeit, kosten aber Start und Datenaustausch. Bei wenig Arbeit oder reinem Warten sind Threads oder
asynciobilliger. - Eine Faustregel ohne Messung glauben. Auch “Threads für I/O, Prozesse für CPU” hat Ausnahmen (C-Erweiterungen, wenig Arbeit). Miss auf deinem Rechner, wie in der Zusatzübung.
Übungen
Übung 1: Little’s Law und Amdahl’s Law als Rechnung (ca. 8 Min.)
Schreibe vier kleine Funktionen:
little(rate, dauer): mittlere Zahl gleichzeitiger AufträgeL = rate * dauer. Beispiellittle(40, 0.25)ergibt10.0.workers_noetig(rate, dauer): wie viele Worker mindestens nötig sind, als ganze Zahl, die nie zu klein ist. Beispielworkers_noetig(7, 0.5)ergibt4(3,5 Aufträge gleichzeitig, also 4 Worker).amdahl(p, n): Beschleunigung bei Parallelanteilp(zwischen 0 und 1) undnWorkern. Beispielamdahl(0.8, 4)ergibt2.5.amdahl_grenze(p): die Obergrenze der Beschleunigung für beliebig viele Worker. Beispielamdahl_grenze(0.9)ergibt (etwa)10. Beip = 1gibt es keine Grenze: gibfloat("inf")zurück.
Bei workers_noetig zählt jeder angebrochene Worker mit. Bei amdahl_grenze fragst du: Was passiert mit p / n, wenn n immer größer wird? Und was bleibt dann im Nenner übrig?
import math
def little(rate, dauer):
return rate * dauer
def workers_noetig(rate, dauer):
return math.ceil(little(rate, dauer))
def amdahl(p, n):
return 1 / ((1 - p) + p / n)
def amdahl_grenze(p):
if p >= 1:
return float("inf")
return 1 / (1 - p)
(little, workers_noetig, amdahl, amdahl_grenze)p / n geht für große n gegen 0, im Nenner bleibt der serielle Anteil 1 - p. Aufrunden bei den Workern, weil 3,5 gleichzeitige Aufträge nicht mit 3 Workern gehen.
Übung 2: Den Engpass vorhersagen (ca. 8 Min.)
Ein Pool von n Workern arbeitet wie in Schritt 2. Jeder Auftrag besteht aus 14 Ticks paralleler Wartezeit und danach 4 Ticks an einer gemeinsamen Ressource (nur ein Worker gleichzeitig). Rechne mit der Formel aus Schritt 2, ohne die Simulation zu starten. Trage ein Tupel (a, b, c, d) ein:
a: Aufträge pro 1000 Ticks bein = 3Workern (Zahl, auf drei Prozent genau reicht)b: Aufträge pro 1000 Ticks bein = 8Workern (auf drei Prozent genau)c: die kleinste Worker-Zahln, ab der ein weiterer Worker den Durchsatz nicht mehr erhöhtd: Wie viele Worker warten bein = 8im Mittel vor der Ressource? (auf 0,3 genau)
Berechne zuerst, welche der beiden Obergrenzen (Worker oder Ressource) bei n = 3 und bei n = 8 die kleinere ist. Für d hilft Little’s Law auf das ganze System: Wie viele der 8 Worker sind in der Warte-Phase, wie viele an der Ressource, wie viele bleiben übrig?
antwort = (166.7, 250, 5, 3.5)
antwortEin Auftrag dauert 18 Ticks. Die Worker-Grenze ist n / 18 pro Tick, die Ressource schafft höchstens 1 / 4 = 0,25 pro Tick. Bei n = 3 ist 3 / 18 = 0,1667 die kleinere Grenze, also 166,7 pro 1000 Ticks. Bei n = 8 ist 8 / 18 = 0,444 größer als 0,25, die Ressource begrenzt: 250 pro 1000 Ticks. Die Worker-Grenze erreicht die Ressourcen-Grenze, wenn n / 18 >= 0,25, also n >= 4,5: ab n = 5 bringt ein weiterer Worker nichts (bei n = 4 sind es erst 4 / 18 = 0,222). Bei n = 8: Durchsatz 0,25, davon 0,25 · 14 = 3,5 Worker in der Warte-Phase, 0,25 · 4 = 1 an der Ressource, bleiben 8 - 3,5 - 1 = 3,5 in der Schlange.
Übung 3: Entscheidungshilfe für Python (MC-Fallstudie, ca. 6 Min.)
Wähle für jeden der drei Fälle die passende Option. Pro Fall ist genau eine Option am besten geeignet. Gib ein Tupel (fall1, fall2, fall3) mit den Buchstaben zurück.
Fall 1. Dein Service muss 3000 Webhooks gleichzeitig an langsame Kunden-Server schicken (Antwortzeit je etwa 10 Sekunden). Es gibt einen async-fähigen HTTP-Client. Der Server hat wenig RAM, die CPU ist fast untätig.
- A: ProcessPoolExecutor mit einem Worker pro Kern
- B: ein Thread pro Webhook, also 3000 Threads
- C: alle Webhooks nacheinander in einer Schleife
- D: eine Coroutine pro Webhook in einem Event Loop
Fall 2. Ein Nachtlauf wertet 40 große Dateien mit reinem Python-Code aus (Schleifen über Zeilen, keine C-Erweiterung). Der Rechner hat 8 Kerne. Jede Datei braucht ähnlich viel Rechenzeit, es wird fast nur gerechnet.
- A: ProcessPoolExecutor mit 8 Workern
- B: ThreadPoolExecutor mit 8 Threads
- C: asyncio mit 40 Tasks im selben Thread
- D: eine einfache Schleife über alle Dateien
Fall 3. Eine Firmenbibliothek hat nur blockierende Funktionen (kein async) und wartet dabei auf das Netzwerk. Du darfst sie nicht ändern. 20 Aufrufe sollen gleichzeitig laufen. Alle Aufrufe teilen sich ein Verbindungsobjekt im Speicher, das sich nicht in andere Prozesse kopieren lässt. Das Auswerten der Ergebnisse ist trivial.
- A: asyncio, die Funktionen direkt in Coroutinen aufrufen
- B: ThreadPoolExecutor mit 20 Threads
- C: ProcessPoolExecutor mit 20 Workern
- D: die 20 Aufrufe nacheinander in einer Schleife
Frage pro Fall: Worauf wartet die Arbeit (Netzwerk oder Rechnen)? Was ist knapp (Speicher, Kerne)? Was lässt sich an der Bibliothek ändern, was nicht? Und wo gibt der GIL den Weg frei, wo steht er im Weg?
antwort = ("D", "A", "B")
antwortFall 1 (D): Reines Warten auf das Netzwerk, sehr viele Verbindungen, wenig RAM, und es gibt einen async-Client. Coroutinen kosten fast nichts pro Stück, 3000 Threads kosten jeweils einen Stack.
Fall 2 (A): Reine Rechnung in Python. Der GIL lässt Threads nicht parallel rechnen, und ein Event Loop auch nicht. Nur Prozesse nutzen mehrere Kerne.
Fall 3 (B): Blockierende Bibliothek, Warten auf das Netzwerk: Der GIL wird beim Warten freigegeben, Threads reichen. Direkt in Coroutinen aufrufen würde den Loop blockieren, Prozesse scheitern am nicht kopierbaren Verbindungsobjekt.
Merksatz und Prüfstein
Merksatz: Mehr Worker helfen nur bis zum Engpass, und in Python entscheidet die Art der Arbeit: Warten (I/O) löst du mit asyncio oder Threads, Rechnen in reinem Python nur mit Prozessen, weil der GIL Threads dort ausbremst.
Prüfstein: Warum bringen mehr Threads nicht automatisch mehr Durchsatz, und woran erkennst du den Engpass?
Der Durchsatz ist durch die knappste Ressource begrenzt (Little’s Law und Amdahl’s Law): Ist ein Teil der Arbeit seriell (Lock, GIL, eine DB-Verbindung), setzt er die Obergrenze, egal wie viele Worker es gibt. Erkennbar an: Auslastung der Ressource nahe 100 Prozent bei flachem Durchsatz, Warteschlange vor der Ressource wächst mit der Worker-Zahl, Latenz pro Auftrag steigt. In Python kommt der GIL dazu: Threads helfen bei I/O-bound Arbeit, bei CPU-bound Arbeit in reinem Python brauchst du Prozesse.
Weiter mit Teil 2: Event Loop, Backpressure und Cancellation.
Quelle: quellen/konzeptuebersicht-software-grundlagen.md, Abschnitt “3. Nebenläufigkeit” (Prozess, Thread, Coroutine, Concurrency vs. Parallelism); quellen/konzeptuebersicht-software-fortgeschritten.docx, Schicht 3 (Modelle, Scheduling, Lastverhalten, Speichermodell). Die Quellen sind Stichwortlisten.
Über die Quelle hinaus (allgemeines Fachwissen): Formeln von Little’s Law und Amdahl’s Law, der Pool-Simulator und die Engpass-Signale, der Aufbau des GIL und seine Auswirkung auf Threads und Prozesse, die Entscheidungstabelle, die Feststellung, dass der GIL Lost Updates nicht verhindert. Zahlen in den Ausgaben stammen aus ausgeführtem Code (Python 3.13.9). Bitte prüfen: Status des free-threaded Builds in Python 3.13 (PEP 703), und welche C-Erweiterungen den GIL freigeben.