Edocti
Fortgeschrittene technische Ausbildung für den modernen Softwareingenieur

Python 3.14 ohne GIL: Wer gewinnt den Kampf zwischen Multithreading und Asyncio?

Artikel geschrieben vom Paul Ianas | Edocti

Python wurde oft für einen "Geburtsfehler" kritisiert: die Unfähigkeit, reinen Python-Code aufgrund des berüchtigten GIL (Global Interpreter Lock) parallel auf mehreren CPU-Kernen auszuführen. Mit der Verabschiedung von PEP 703 und der Stabilisierung des Free-Threaded-Builds in Python 3.13 und 3.14 ist diese Barriere jedoch endlich verschwunden.

In diesem Artikel analysieren wir die geschäftlichen Auswirkungen, wie der GIL entstand, wie er auf C-Ebene funktionierte, wie er entfernt wurde und was dies für das klassische Duell bedeutet: Multithreading vs. Asyncio.

1. Geschäftliche Auswirkungen: Warum ist das Tandem No-GIL + Asyncio wichtig?

Bevor wir die Anatomie des GIL auf C-Code-Ebene zerlegen, lohnt es sich, die Auswirkungen dieser Änderung auf Softwarearchitektur-Entscheidungen zu untersuchen. Python war schon immer ein Champion in Sachen Time-to-Market, ideal für schnelles Prototyping und das Daten-Ökosystem. Die Skalierung rechenintensiver (CPU-bound) Aufgaben zwang Teams jedoch aufgrund des GIL zu teuren Kompromissen.

Mit Free-Threaded Python und der Reifung von Asyncio erreichen wir ein hervorragendes Gleichgewicht zwischen Entwicklungsgeschwindigkeit und reiner Leistung:

  • Optimierung des Speicherverbrauchs in der Cloud: Um 16 Kerne auf einem Server zu nutzen, war standardmäßig multiprocessing erforderlich. Aufgrund der Art und Weise, wie Python Speicherreferenzen verwaltet, vervielfachten diese Prozesse schnell ihren RAM-Bedarf. Jetzt, mit No-GIL, können diese 16 Kerne von Threads genutzt werden, die denselben Speicherplatz teilen. Sie erhalten eine höhere Anwendungsdichte bei gleichen Hardware-Ressourcen.
  • Vermeidung von Neuschreibungen kritischer Module: Ein gängiges Skalierungsmuster besteht darin, Engpässe (Bottlenecks) zu identifizieren und diese Dienste in Go, Rust oder C++ neu zu schreiben. No-GIL ermöglicht die vertikale Skalierung direkt in Python für eine viel längere Zeit, wodurch eine einheitliche Codebasis erhalten bleibt und Wartungskosten gesenkt werden.
  • Einfachere Softwarearchitekturen: Wenn ein Webserver schnell antworten, aber auch schwere Verarbeitungsaufgaben bewältigen musste, war man gezwungen, die Logik in angrenzende Systeme (Redis, Celery) auszulagern. Dank der Symbiose aus Asyncio + Threads wird die Architektur vereinfacht. Eine Event-Loop verarbeitet den Web-Traffic effizient, und CPU-gebundene Aufgaben werden direkt an einen lokalen Thread-Pool delegiert.
  • Optimierte Data Engineering Workflows: Datenvorbereitungsphasen (ETL, Data Ingestion) in reinem Python blockierten oft auf einem einzigen Kern und verlangsamten die Einspeisung von KI-Modellen. Die native Parallelisierung beseitigt diese Engpässe in Datenpipelines.

2. Eine kurze Geschichte des Multithreading in Python

Als Guido van Rossum Python Anfang der 90er Jahre entwickelte, verfügten Consumer-Computersysteme über einen einzigen Kern (Single-Core). Die Speicherverwaltung in Python basierte auf Reference Counting (Referenzzählung).

Damit das System threadsicher (thread-safe) ist, musste die gleichzeitige Änderung des Referenzzählers eines Objekts durch zwei Threads geschützt werden. Die einfachste und effizienteste Lösung zu dieser Zeit war die Einführung einer einzigen globalen Sperre: der GIL.

Obwohl sich Computer in Richtung Multi-Core-Architekturen entwickelten, erwies sich die Entfernung des GIL als äußerst schwierig, da frühere Versuche die Single-Thread-Leistung zerstörten und in C geschriebene Erweiterungen (wie NumPy) stark auf das vorhersehbare Verhalten des GIL angewiesen waren.

3. Was macht der GIL auf Makroebene? Der C-Mechanismus

GIL vs Free-Threaded vs Asyncio

Auf der Ebene des offiziellen Interpreters (CPython), ist der GIL im Wesentlichen ein POSIX-Mutex (pthread_mutex_t). Er garantiert, dass jeweils nur ein Python-Thread Anweisungen (Bytecode) ausführen kann.

Wenn wir das Verhalten von CPython aus früheren Versionen vereinfachen, sah der Code, der für die Ausführung von Threads verantwortlich ist, konzeptionell so aus:

#include <pthread.h>

// Vereinfachte Struktur des GIL in CPython
typedef struct {
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    int locked;
} GIL_t;

GIL_t gil;

void take_gil() {
    pthread_mutex_lock(&gil.mutex);
    while (gil.locked) {
        pthread_cond_wait(&gil.cond, &gil.mutex);
    }
    gil.locked = 1;
    pthread_mutex_unlock(&gil.mutex);
}

void drop_gil() {
    pthread_mutex_lock(&gil.mutex);
    gil.locked = 0;
    pthread_cond_signal(&gil.cond);
    pthread_mutex_unlock(&gil.mutex);
}

// Haupt-Auswertungsschleife
void* interpreter_loop(void* arg) {
    while (has_bytecode_to_execute) {
        take_gil();

        // Führt eine begrenzte Anzahl von Bytecode-Anweisungen aus
        execute_python_bytecode();

        drop_gil();
        // Der Thread gibt anderen Threads die Chance, den GIL zu übernehmen
    }
}

Die Auswirkungen: Aufgrund dieser Struktur blieb die CPU-Auslastung bei 100% (das Äquivalent eines einzigen Kerns), wenn man 16 Kerne hatte und ein Skript mit 16 Threads ausführte, die mathematische Berechnungen durchführten. Darüber hinaus kämpften die Threads ständig um den Mutex, was durch Context Switching massiven Overhead erzeugte.

4. Die Free-Threaded Revolution: Wie der GIL entfernt wurde

Die Entfernung des GIL in Python 3.14 bedeutete eine tiefgreifende Umschreibung der Art und Weise, wie CPython den Speicher verwaltet, wobei drei Haupttechniken eingesetzt wurden:

  • Biased Reference Counting (BRC): Objekte haben nun zwei Referenzzähler. Einen lokalen Zähler (der nur von dem Thread geändert wird, der das Objekt erstellt hat) und einen gemeinsamen Zähler (der atomar von anderen Threads geändert wird).
  • Mimalloc: Ein extrem schneller und threadsicherer Speicherzuweiser.
  • Thread-Safe Collections: Wörterbücher und Listen verwenden nun granulare Sperrmechanismen (pro Objekt) anstelle einer globalen Sperre.

Anstatt einen globalen Mutex freizugeben und zu übernehmen, verwendet die Objektmanipulation jetzt atomare Anweisungen direkt auf Prozessorebene:

// Vorher (Mit GIL):
static inline void Py_INCREF(PyObject *op) {
    // Sicher, der GIL garantiert, dass niemand sonst diesen Speicher gleichzeitig ändert
    op->ob_refcnt++;
}

// Jetzt (Ohne GIL / Free-Threaded):
static inline void Py_INCREF(PyObject *op) {
    // Verwendet atomare CPU-Operationen (z.B. LOCK XADD auf x86) oder BRC
    if (_Py_IsOwnedByCurrentThread(op)) {
        op->ob_refcnt_local++;
    } else {
        atomic_fetch_add(&op->ob_refcnt_shared, 1);
    }
}

5. Benchmark: Leistungstests in Python 3.14

Um den neuen Ausführungsmodus zu demonstrieren, verwenden wir ein SMP-Szenario (Symmetric Multiprocessing) ohne Kommunikation zwischen Threads. Dieser Test teilt die Verarbeitung großer Arrays gleichmäßig auf die Kerne auf.

import threading
import time

N = 16_000_000
NCORES = 16

a = [i for i in range(N)]
b = [i+1 for i in range(N)]
c = [0] * N

def add(start, end):
    for times in range(100):
        for i in range(start, end):
            c[i] = a[i] + b[i]

if __name__ == "__main__":
    print(f"Starte Benchmark auf {NCORES} Kernen...")
    start_time = time.perf_counter()

    workers = []
    CHUNK_SIZE = N // NCORES

    for i in range(NCORES):
        start = i * CHUNK_SIZE
        end = start + CHUNK_SIZE
        t = threading.Thread(target=add, name=f'Worker-{i+1}', args=(start, end))
        workers.append(t)
        t.start()

    for w in workers:
        w.join()

    end_time = time.perf_counter()
    print(f"Benchmark beendet in {end_time - start_time:.4f} Sekunden.")
    print(f"c[:5]: {c[:5]}")

Um den direkten Unterschied zu sehen, führen Sie das Skript in beiden Modi aus:

  • Mit aktiviertem GIL: python3.14 -X gil=1 benchmark.py
  • Ohne GIL: python3.14 -X gil=0 benchmark.py

6. Multithreading vs. Asyncio: Wer gewinnt den Kampf in der Free-Threaded-Ära?

Traditionell gab es in Python eine klare Trennung: Asyncio für Netzwerkoperationen (I/O) und Multiprocessing für mathematische Berechnungen (CPU). Die Entfernung des GIL definiert diese Rollen neu.

Asyncio: Das Cooperative Scheduling Paradigma

Asyncio ist kein echtes paralleles System und hat auch nie versucht, eines zu sein. Es funktioniert als Cooperative Scheduler, basierend auf einem Single-Task-Ausführungsmodell.

In einem kooperativen System sind Aufgaben (Tasks) "höflich". Ein Task läuft auf dem Ausführungsthread, bis er auf eine await-Anweisung trifft. In diesem Moment gibt der Task freiwillig die CPU-Kontrolle ab und unterbricht sich, wodurch der Staffelstab zurück an die Event Loop übergeben wird. Die Event Loop überprüft die Ereignistabelle des Betriebssystems und weckt den nächsten Task auf, sobald das erwartete Ereignis eintritt.

Hier ist ein praktisches Beispiel, das die Stärke von Asyncio für I/O-bound Operationen (z. B. gleichzeitige HTTP-Aufrufe) demonstriert:

"""
Der Client sendet alle 100 Anfragen fast augenblicklich
auf einem einzigen Thread an den Server. Danach fängt die Event Loop
die Antworten ab, sobald sie über das Netzwerk eintreffen.
Geschätzte Gesamtzeit auf Client-Seite: ~2 Sekunden (nur durch Netzwerklatenz begrenzt).
"""
import asyncio
import httpx  # Erfordert die Installation: pip install httpx
import time

async def fetch_data(client, task_id):
    print(f"Task {task_id}: Sende Anfrage...")

    # Das Zauberwort 'await' ist der Schlüssel der Architektur.
    # An diesem Punkt sagt der aktuelle Task:
    # "Ich warte auf die Serverantwort. Event Loop, übernimm die Kontrolle und starte einen anderen Task!"
    response = await client.get('http://127.0.0.1:5000/slow')

    print(f"Task {task_id}: Antwort erhalten (Status: {response.status_code})")
    return response.status_code

async def main():
    # Öffne eine einzelne persistente Sitzung (Best Practice für Performance)
    async with httpx.AsyncClient(timeout=20.0) as client:
        start_time = time.time()

        # Bereite die Liste der Koroutinen vor (sie werden noch nicht ausgeführt)
        tasks = [fetch_data(client, i) for i in range(1, 101)]

        # Starte alle 100 Tasks nebenläufig (concurrently)
        await asyncio.gather(*tasks)

        total_time = time.time() - start_time
        print(f"\n--- Fertig! 100 Anfragen in {total_time:.2f} Sekunden verarbeitet ---")

if __name__ == "__main__":
    # Einstiegspunkt in die Event Loop
    asyncio.run(main())

Der große Vorteil: Auf der Ebene des Python-Codes gibt es keine Race Conditions. Es läuft jeweils nur ein Task, wodurch komplexe Sperren (Locks) überflüssig werden, während die Netzwerkeffizienz maximal bleibt.

Multithreading: Preemptive, aber auf Kosten des Speichers

Im Gegensatz zu Asyncio ist Multithreading preemptive (präemptiv). Das Betriebssystem entscheidet, wann ein Thread unterbrochen wird, völlig unvorhersehbar für den Programmierer.

Wenn wir jedoch den Benchmark aus Kapitel 5 ausführen, werden wir eine wichtige Tatsache bemerken: Selbst ohne den GIL springt die CPU-Auslastung bei reinem Python-Code nicht magisch auf 1600%. Der Grund? Wenn 16 Threads Millionen von neuen Objekten (Ganzzahlen in Python) zuweisen und manipulieren, werden der Speicherbus und die Aktualisierung der Referenzzähler (BRC) zum neuen Bottleneck.

Multithreading glänzt in Python 3.14 wirklich dann, wenn Threads massive Operationen in C/C++ oder Rust-Erweiterungen (wie NumPy) ausführen, die den Thread vom Overhead der Python-Objektverwaltung befreien.

Vergleichsmatrix

Kriterium Multithreading (Free-Threaded) Asyncio
Scheduling-Modell Preemptive (Betriebssystem entscheidet). Cooperative (Tasks geben CPU freiwillig ab).
Ausführung Multi-Task, echte Parallelität über mehrere Kerne. Single-Task auf einem einzigen logischen Thread.
Thread-Sicherheit Komplex. Hohes Risiko von Race Conditions. Erfordert Locks. Einfach. Implizit sicher auf Code-Ebene.
Bester Anwendungsfall CPU-intensive Aufgaben, native Erweiterungen, massive isolierte Daten. I/O-bound Anwendungen: Webserver, Proxys, APIs.
Schwachpunkt Speicher-Overhead durch Objektverwaltung. Eine einzige schwere Non-Async-Berechnung friert die Event Loop ein.

Das Fazit: Perfekte Symbiose

Wer gewinnt? Keiner eliminiert den anderen; wir haben es mit einer Symbiose zu tun.

In Python 3.14 können Sie die elegante Architektur einer Asyncio Event Loop beibehalten, um Zehntausende von Webverbindungen von Clients zu verwalten. Wenn eine schwere Verarbeitungsaufgabe auftritt (CPU-bound), können Sie diese direkt an einen nativen Thread-Pool senden (loop.run_in_executor), im Wissen, dass dieser Thread auf einem separaten Kern im selben Speicherbereich läuft, ohne die Event Loop zu blockieren und ohne vom GIL gebremst zu werden. Der wahre Gewinner ist der Entwickler.

Bereit, moderne Architekturen in Python zu meistern?

Wenn Ihnen diese Low-Level-Leistungsanalyse gefallen hat, machen Sie mit uns den nächsten Schritt. Lernen Sie, komplexe Softwaresysteme zu entwerfen und zu optimieren.

Entdecken Sie Fortgeschrittene Python-Themen

Über diesen Newsletter

Dieses Material ist Teil einer Reihe technischer Artikel, die vom Team von Edocti erstellt wurden. Wir pflegen einen technischen, detaillierten Ton, der in realen Produktionsherausforderungen verankert ist. Wir konzentrieren uns auf Leistung, verteilte Systeme, systemnahe Programmierung und fortschrittliche Lösungen in den Bereichen Deep Learning und Computer Vision.