Asynchrone vs. Synchrone Funktionen in FastAPI: Wann welche wählen?
Olivia Novak
Dev Intern · Leapcell

Einleitung
Die Entwicklung effizienter und skalierbarer Webanwendungen ist ein ständiges Bestreben für Entwickler. Im Python-Ökosystem hat sich FastAPI als Kraftpaket für die Erstellung von Hochleistungs-APIs etabliert, was zu einem großen Teil auf seine asynchronen Fähigkeiten zurückzuführen ist. Ein häufiger Verwirrungspunkt für Neulinge und sogar erfahrene Entwickler, die zu FastAPI wechseln, ist jedoch das Verständnis, wann async def verwendet werden soll und wann man bei einer traditionellen def-Funktion bleiben sollte. Diese Entscheidung ist nicht nur stilistisch, sondern hat tiefgreifende Auswirkungen auf die Reaktionsfähigkeit, Ressourcennutzung und die Gesamtleistung Ihrer Anwendung. Dieser Artikel wird die Unterschiede zwischen asynchronen und synchronen Funktionsdefinitionen in FastAPI entmystifizieren und klare Anleitungen geben, wann jede zu verwenden ist, um Ihnen letztendlich den Aufbau robusterer und effizienterer Webdienste zu ermöglichen.
Die Kernkonzepte verstehen
Bevor wir uns mit den Besonderheiten von FastAPI befassen, ist es wichtig, die grundlegenden Konzepte der synchronen und asynchronen Programmierung in Python zu verstehen.
Synchrone Funktionen (def)
Wenn Sie eine Funktion mit def definieren, gilt sie als synchron. Das bedeutet, dass die Funktion, wenn sie aufgerufen wird, ihre Operationen sequenziell, nacheinander ausführt. Wenn eine synchrone Funktion auf eine Operation stößt, die lange dauert (z. B. Warten auf eine Datenbankabfrage, einen externen API-Aufruf oder Datei-I/O), blockiert das gesamte Programm an dieser Stelle und wartet, bis die Operation abgeschlossen ist, bevor es zur nächsten Codezeile übergeht. Im Kontext eines Webservers bedeutet dies, dass der Server keine anderen eingehenden Anfragen auf demselben Worker-Thread verarbeiten kann, während eine Anfrage eine blockierende I/O-Operation durchführt.
import time def synchronous_task(task_id: int): print(f"Synchronous Task {task_id}: Starting CPU-bound work...") # Simulate a CPU-bound operation count = 0 for _ in range(1_000_000_000): count += 1 print(f"Synchronous Task {task_id}: CPU-bound work finished.") print(f"Synchronous Task {task_id}: Starting I/O-bound wait...") # Simulate a blocking I/O operation time.sleep(2) # This will block the thread print(f"Synchronous Task {task_id}: I/O-bound wait finished.") return f"Result from synchronous task {task_id}" # When run, synchronous_task(1) would complete entirely before synchronous_task(2) starts.
Asynchrone Funktionen (async def)
Mit async def definierte Funktionen sind asynchron, d. h. sie sind so konzipiert, dass sie Operationen gleichzeitig ausführen, ohne den Hauptausführungsthread zu blockieren. Das Schlüsselwort await ist innerhalb von async def-Funktionen entscheidend. Wenn eine async def-Funktion auf einen await-Ausdruck für ein "awaitable"-Objekt stößt (wie asyncio.sleep, ein asynchroner HTTP-Request mit httpx oder ein asynchroner Datenbanktreiberaufruf), pausiert sie ihre Ausführung an dieser Stelle und gibt die Kontrolle an die Ereignisschleife zurück. Die Ereignisschleife kann dann zu einer anderen Aufgabe wechseln, die zur Ausführung bereit ist. Sobald die erwartete Operation abgeschlossen ist, kann die async def-Funktion ihre Ausführung von dort fortsetzen, wo sie aufgehört hat. Dieses nicht-blockierende Verhalten ist besonders vorteilhaft für I/O-gebundene Aufgaben.
import asyncio async def asynchronous_task(task_id: int): print(f"Asynchronous Task {task_id}: Starting I/O-bound wait...") # Simulate a non-blocking I/O operation await asyncio.sleep(2) # This will yield control to the event loop print(f"Asynchronous Task {task_id}: I/O-bound wait finished.") print(f"Asynchronous Task {task_id}: Starting CPU-bound work...") # Simulate a CPU-bound operation (still blocks if not offloaded) count = 0 for _ in range(1_000_000_000): count += 1 print(f"Asynchronous Task {task_id}: CPU-bound work finished.") return f"Result from asynchronous task {task_id}" # In an async context, multiple calls to asynchronous_task might run 'side-by-side' during their await periods.
FastAPI und Funktionsausführung
FastAPI, das auf Starlette und Pydantic basiert, nutzt die asyncio-Bibliothek von Python, um eine asynchrone Anforderungsbearbeitung zu ermöglichen. Dies ermöglicht ihm, eine hohe Nebenläufigkeit mit einer relativ geringen Anzahl von Worker-Prozessen zu erreichen.
Wie FastAPI async def behandelt
Wenn FastAPI eine Anfrage erhält und diese an eine async def-Endpunktfunktion weiterleitet, führt es diese Funktion direkt innerhalb seiner Ereignisschleife aus. Wenn die async def-Funktion auf eine I/O-gebundene Operation stößt, die awaited wird, gibt sie die Kontrolle ab und ermöglicht der Ereignisschleife, mit der Verarbeitung anderer eingehender Anfragen oder anderer bereitstehender Aufgaben fortzufahren. Hier liegt die Stärke von async def: Bei I/O-gebundenen Operationen (Datenbankaufrufe, externe API-Aufrufe, Datei-Lese-/Schreibvorgänge, Netzwerkanfragen) kann Ihre Anwendung viele gleichzeitige Clients effizient bedienen, ohne für jede wartende Operation einen dedizierten Thread zu benötigen.
Beispiel: async def für I/O-gebundene Operationen
Betrachten Sie einen API-Endpunkt, der Daten von einem externen Dienst abruft.
from fastapi import FastAPI import httpx # An async HTTP client import asyncio app = FastAPI() @app.get("/items_async/{item_id}") async def get_item_async(item_id: int): print(f"Request for item {item_id}: Starting external API call asynchronously...") async with httpx.AsyncClient() as client: # Simulate an external API call that takes time response = await client.get(f"https://jsonplaceholder.typicode.com/todos/{item_id}") data = response.json() print(f"Request for item {item_id}: External API call finished.") return {"item_id": item_id, "data": data} # To test this, you could make multiple concurrent requests to /items_async/1, /items_async/2, etc. # You'd observe that they complete in an interleaved fashion, not strictly one after another.
In diesem Beispiel pausiert await client.get(...) die Ausführung von get_item_async, ohne die Haupt-Ereignisschleife zu blockieren. FastAPI kann dann andere eingehende Anfragen bearbeiten oder andere Aufgaben ausführen.
Wie FastAPI def behandelt
Wenn FastAPI eine def-Endpunktfunktion feststellt, erkennt es diese intelligent als synchrone Funktion. Um zu verhindern, dass synchrone Funktionen seine Haupt-Ereignisschleife blockieren, führt FastAPI synchrone Endpunktfunktionen automatisch in einem separaten Thread-Pool aus. Das bedeutet, dass, wenn Ihre def-Funktion eine blockierende I/O-Operation oder eine lange CPU-gebundene Berechnung durchführt, sie einen Thread aus diesem Thread-Pool blockiert, aber nicht die Haupt-Ereignisschleife selbst sperrt.
Beispiel: def für synchrone, potenziell blockierende Operationen
Stellen Sie sich einen Endpunkt vor, der eine komplexe, CPU-intensive Berechnung durchführt, die nicht einfach asynchron gemacht werden kann.
from fastapi import FastAPI import time app = FastAPI() def perform_heavy_computation(number: int): print(f"Synchronous Computation for {number}: Starting CPU-bound work...") # Simulate a CPU-bound operation result = 0 for i in range(number * 10_000_000): # A big loop result += i print(f"Synchronous Computation for {number}: CPU-bound work finished.") return result @app.get("/compute_sync/{number}") def compute_sync(number: int): print(f"Request for computation {number}: Received.") computation_result = perform_heavy_computation(number) return {"input_number": number, "result": computation_result} # If multiple requests hit /compute_sync concurrently, each will run in a separate thread from FastAPI's thread pool. # The number of concurrent synchronous operations is limited by the size of this thread pool.
In diesem Fall ist perform_heavy_computation eine blockierende Funktion. FastAPI führt sie in einem Hintergrund-Thread aus und verhindert so, dass sie die Haupt-Ereignisschleife blockiert. Die Anzahl solcher gleichzeitigen blockierenden Operationen ist jedoch durch die Größe des Thread-Pools begrenzt (standardmäßig oft etwa 40 Threads für uvicorn), und das Erstellen und Verwalten von Threads verursacht Overhead.
Wann async def vs. def verwenden?
Die Wahl zwischen async def und def hängt in erster Linie von der Art der Operationen ab, die Ihr Endpunkt ausführt.
Verwenden Sie async def, wenn:
- Ihre Funktion I/O-gebundene Vorgänge beinhaltet, die
awaited werden können. Dies ist der primäre Anwendungsfall. Beispiele hierfür sind:- HTTP-Anfragen an externe APIs mit
httpxmachen. - Interaktion mit asynchronen Datenbanktreibern (z. B.
asyncpgfür PostgreSQL,aioodbc,SQLModelmitasyncio). - Asynchrones Lesen/Schreiben von Dateien (z. B.
aiofiles). - Warten auf Nachrichten von einer asynchronen Warteschlange.
- Jede Operation, die das Warten auf eine externe Ressource ohne intensive CPU-Nutzung beinhaltet.
- HTTP-Anfragen an externe APIs mit
- Sie müssen andere
awaitable Utilities oder Bibliotheken nutzen. Wenn Sie mit Bibliotheken integrieren, die von Natur aus asynchron sind, istasync deferforderlich, um deren Operationen zuawaiten. - Sie möchten die Nebenläufigkeit für I/O-gebundene Aufgaben maximieren.
async defermöglicht Ihrer Anwendung, ein hohes Volumen gleichzeitiger Anfragen effizient zu bearbeiten, solange diese Anfragen die meiste Zeit mit dem Warten auf I/O verbringen.
Faustregel: Wenn Ihre Funktion das Schlüsselwort await enthält, muss sie async def sein.
Verwenden Sie def, wenn:
- Ihre Funktion rein CPU-gebundene Operationen durchführt. Wenn Ihre Funktion die meiste Zeit mit Berechnungen, der Verarbeitung von Daten im Speicher oder umfangreichen Schleifen ohne Warten auf externe Ressourcen verbringt, ist sie CPU-gebunden. Sie als
async defzu kennzeichnen, macht die CPU-Berechnung nicht magisch nicht-blockierend. Tatsächlich blockiert die eigentliche Berechnung weiterhin die Ereignisschleife (wenn sie nichtawaited wird) oder einen Hintergrund-Thread (wenn FastAPI einedef-Funktion auslagert).- Beispiele: Komplexe mathematische Berechnungen, schwere Datentransformationen, Bildbearbeitung, Videokodierung.
- Ihre Funktion mit synchronen Bibliotheken oder Treibern interagiert, die nur synchron sind. Viele ältere Python-Bibliotheken, insbesondere Datenbanktreiber (wie
psycopg2für PostgreSQL oder die traditionelle Form desSQLAlchemy-ORM), sind synchron. Wenn Ihr Endpunkt diese verwenden muss, ermöglicht die Definition alsdef, dass FastAPI die blockierende Natur durch Ausführung in einem Thread-Pool handhabt. - Einfachheit und Vertrautheit. Bei sehr einfachen Endpunkten, die keine I/O oder komplexe Logik beinhalten, kann eine
def-Funktion geringfügig einfacher zu schreiben und zu verstehen sein, insbesondere wenn Sie mit den Best Practices vonasyncionicht tief vertraut sind. Berücksichtigen Sie jedoch immer die zukünftige Skalierbarkeit.
Wichtiger Hinweis zu CPU-gebundenen Operationen aus async def: Wenn eine async def-Funktion einen CPU-gebundenen Code-Teil enthält, der nichts aktiv awaited, blockiert dieser CPU-gebundene Code weiterhin die Ereignisschleife. Um CPU-gebundene Arbeiten innerhalb eines async def-Endpunkts auszuführen, ohne die Ereignisschleife zu blockieren, würden Sie sie normalerweise in einen separaten Prozess oder einen Thread-Pool mit loop.run_in_executor() (oder darauf basierenden Bibliotheken wie starlette.concurrency.run_in_threadpool) auslagern. FastAPI macht dies automatisch für def-Funktionen, aber für async def-Funktionen müssen Sie dies explizit verwalten, wenn Sie langlaufenden CPU-Code haben.
from concurrent.futures import ThreadPoolExecutor from functools import partial # ... (assume app = FastAPI() and the perform_heavy_computation function from above) executor = ThreadPoolExecutor(max_workers=4) # A thread pool for CPU-bound tasks @app.get("/compute_async_offloaded/{number}") async def compute_async_offloaded(number: int): print(f"Request for computation {number}: Received, offloading CPU work...") # Offload the intensive CPU computation to a thread pool loop = asyncio.get_event_loop() computation_result = await loop.run_in_executor( executor, partial(perform_heavy_computation, number) ) return {"input_number": number, "result": computation_result}
Dies ist ein fortgeschritteneres Muster und zeigt, dass manchmal selbst async def-Funktionen explizit CPU-gebundene Arbeiten verwalten müssen.
Fazit
Die Wahl zwischen async def und def in FastAPI ist eine kritische Entscheidung, die die Leistungseigenschaften Ihrer Anwendung beeinflusst. Für I/O-gebundene Aufgaben ist async def fast immer die überlegene Wahl, die hohe Nebenläufigkeit und effiziente Ressourcennutzung ermöglicht, indem sie die asyncio-Ereignisschleife von Python nutzt. Umgekehrt ist für CPU-gebundene Operationen oder Interaktionen mit synchronen Bibliotheken def angemessen, wobei FastAPI diese intelligent in einen Thread-Pool auslagert, um die Blockierung seiner Haupt-Ereignisschleife zu verhindern. Indem Sie die zugrunde liegenden Mechanismen verstehen und die Art Ihrer Operationen berücksichtigen, können Sie beide Paradigmen effektiv nutzen, um hochperformante und skalierbare FastAPI-Anwendungen zu erstellen. Im Zweifelsfall bevorzugen Sie async def, wenn ein Teil Ihrer Funktion I/O-gebunden ist und asynchron awaited werden kann.

