Asynchrone Web Services in Rust: Ein tiefer Einblick in Future, Tokio und async/await
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Einleitung
Im Bereich der modernen Webentwicklung sind Reaktionsfähigkeit und Skalierbarkeit von größter Bedeutung. Traditionelle synchrone Programmiermodelle kämpfen oft damit, den Anforderungen gleichzeitiger I/O-Operationen gerecht zu werden, was zu blockierten Threads und einer ineffizienten Ressourcennutzung führt. Hier glänzt die asynchrone Programmierung: Sie ermöglicht es Anwendungen, nicht-blockierende Operationen durchzuführen und viele Anfragen gleichzeitig zu bearbeiten, ohne die Leistung zu beeinträchtigen. Rust hat sich mit seinem starken Fokus auf Sicherheit, Leistung und Nebenläufigkeit schnell als ausgezeichnete Wahl für die Erstellung robuster Webdienste etabliert. Der Schlüssel zur Erschließung des asynchronen Potenzials von Rust liegt im Verständnis eines Trios grundlegender Konzepte: des Future
-Traits, der Tokio-Laufzeitumgebung und der async/await
-Syntax. Diese Untersuchung wird sich mit diesen Kernkomponenten befassen und veranschaulichen, wie sie zusammenwirken, um eine effiziente und leistungsstarke asynchrone Webentwicklung in Rust zu ermöglichen.
Erklärte Kernkonzepte
Bevor wir uns mit den Mechanismen befassen, wollen wir ein klares Verständnis der grundlegenden Begriffe schaffen, die das asynchrone Ökosystem von Rust untermauern.
Future-Trait
In Rust ist Future
ein Trait, der eine asynchrone Berechnung darstellt, die zu einem späteren Zeitpunkt abgeschlossen werden kann. Es handelt sich um eine enum-ähnliche Zustandsmaschine, die abgefragt werden kann, um ihren Fortschritt zu überprüfen. Wenn ein Future
abgefragt wird, kann er einen von zwei Zuständen zurückgeben:
Poll::Pending
: Das Future ist noch nicht fertig, und die Aufgabe sollte später erneut abgefragt werden.Poll::Ready(T)
: Das Future wurde abgeschlossen und liefert einen Wert vom TypT
.
Die Kernmethode des Future
-Traits ist poll(&mut self, cx: &mut Context<'_>) -> Poll<Self::Output>
. Der Context
bietet Zugriff auf einen Waker
, der entscheidend ist, um den Executor zu benachrichtigen, wenn das Future bereit ist, erneut abgefragt zu werden, nachdem es Pending
war. Entscheidend ist, dass Future
s "lazy" sind; sie tun nichts, bis sie ausdrücklich von einem Executor abgefragt werden.
Tokio-Laufzeitumgebung
Die Tokio-Laufzeitumgebung ist eine asynchrone Laufzeitumgebung für Rust, die alles bereitstellt, was zum Ausführen von asynchronem Code benötigt wird. Sie wird oft als "Async Executor" bezeichnet, da sie Future
s nimmt und sie "ausführt", indem sie sie wiederholt abfragt, bis sie abgeschlossen sind. Tokio bietet mehr als nur einen Executor; es bietet ein umfassendes Ökosystem, das Folgendes umfasst:
- Ein Multi-Thread-Scheduler: Verteilt
Future
s effizient über mehrere Threads. - Asynchrone I/O-Primitive: Nicht-blockierende Versionen gängiger I/O-Operationen (TCP, UDP, Dateien usw.).
- Timer: Zum Planen von Operationen zu bestimmten Zeiten oder nach einer Verzögerung.
- Synchronisationsprimitive: Async-fähige Mutexe, Semaphoren und Kanäle.
Tokio kümmert sich um die komplexen Details der Thread-Verwaltung, der Task-Planung und der I/O-Multiplexierung, sodass sich Entwickler auf die Anwendungslogik konzentrieren können.
async/await-Syntax
Die async/await
-Syntax in Rust bietet eine ergonomischere Möglichkeit, asynchronen Code zu schreiben und zu komponieren, sodass er sich wie synchroner Code anfühlt und aussieht.
- Das
async
-Schlüsselwort wandelt eine Funktion oder einen Block in eine asynchrone Funktion oder einen Block um, der ein anonymesFuture
zurückgibt. Wenn Sie eineasync
-Funktion aufrufen, gibt sie sofort einFuture
zurück, ohne ihren Körper auszuführen. Der Körper derasync
-Funktion wird erst ausgeführt, wenn das zurückgegebeneFuture
abgefragt wird. - Das
await
-Schlüsselwort kann nur innerhalb einerasync
-Funktion oder einesasync
-Blocks verwendet werden. Es pausiert die Ausführung der aktuellenasync
-Funktion, bis dasFuture
, auf das es wartet, abgeschlossen ist. Währendawait
wartet, blockiert es den aktuellen Thread nicht; stattdessen gibt es die Kontrolle an den Executor zurück und ermöglicht die Ausführung andererFuture
s. Sobald dasawait
-edFuture
bereit ist, wird dieasync
-Funktion von dort fortgesetzt, wo sie aufgehört hat.
Prinzipien, Implementierung und Anwendung
Lassen Sie uns veranschaulichen, wie diese Konzepte zusammenwirken, um asynchrone Webdienste zu erstellen.
Der asynchrone Arbeitsablauf illustriert
Betrachten Sie eine einfache asynchrone Funktion, die eine I/O-Operation simuliert:
async fn fetch_data_from_remote() -> String { println!("Fetching data..."); // Simulate a network request that takes time tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; println!("Data fetched!"); "Hello from remote server!".to_string() }
Diese async fn
gibt ein Future<Output = String>
zurück. Wenn fetch_data_from_remote()
aufgerufen wird, wird sie nicht sofort ausgeführt; sie erstellt nur das Future
. Das await
innerhalb der Funktion gibt die Kontrolle zurück und ermöglicht es dem Tokio-Laufzeitumgebung, das tokio::time::sleep
-Future zu verarbeiten, ohne den Thread zu blockieren.
Um dieses Future
auszuführen, benötigen wir einen Executor, den Tokio bereitstellt:
#[tokio::main] async fn main() { println!("Starting application..."); // Calling an async function returns a Future let future_data = fetch_data_from_remote(); // The AWAIT keyword polls the Future until it completes. // While awaiting, the main thread is not blocked. let data = future_data.await; println!("Received: {}", data); println!("Application finished."); }
Das Attribut #[tokio::main]
ist ein praktisches Makro, das von Tokio bereitgestellt wird und eine Tokio-Laufzeitumgebung einrichtet und dann die async fn main()
-Funktion innerhalb dieser Laufzeitumgebung ausführt. Ohne #[tokio::main]
würden Sie manuell eine Laufzeitumgebung erstellen, wie folgt:
fn main() { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { println!("Starting application..."); let data = fetch_data_from_remote().await; println!("Received: {}", data); println!("Application finished."); }); }
rt.block_on()
führt ein einzelnes Future
bis zur Fertigstellung auf dem aktuellen Thread aus und blockiert, bis dieses Future
abgeschlossen ist. Obwohl block_on
selbst blockierend ist, kann das von ihm ausgeführte Future
(in diesem Fall unser async
-Block) bei await
die Kontrolle an den Executor zurückgeben.
Erstellen eines einfachen asynchronen Webservers mit Axum
Mal sehen, wie diese Konzepte bei der Erstellung eines einfachen asynchronen Webservers mit Axum, einem auf Tokio basierenden Webframework, zusammenwirken.
Fügen Sie zunächst die notwendigen Abhängigkeiten zu Cargo.toml
hinzu:
[dependencies] tokio = { version = "1", features = ["full"] } axum = "0.7"
Implementieren Sie nun einen einfachen Server:
use axum::{ routing::get, Router, }; use std::net::SocketAddr; // Eine asynchrone Handler-Funktion async fn hello_world() -> String { println!("Handling /hello request..."); // Simulate some asynchronous work tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; "Hello, Axum and async Rust!".to_string() } // Eine weitere asynchrone Handler-Funktion mit Pfadparametern async fn greet_user(axum::extract::Path(name): axum::extract::Path<String>) -> String { println!("Handling /greet request for: {}", name); tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; format!("Greetings, {}! Welcome to async Rust.", name) } #[tokio::main] async fn main() { // Build our application router let app = Router::new() .route("/hello", get(hello_world)) .route("/greet/:name", get(greet_user)); // Define the address to listen on let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("Listening on {}", addr); // Run the server with hyper (Axum's underlying HTTP library), which uses Tokio axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); }
In diesem Beispiel:
hello_world
undgreet_user
sindasync fn
s. Wenn eine HTTP-Anfrage für/hello
oder/greet/:name
eingeht, ruft Axum diese Funktionen auf, die sofortFuture
s zurückgeben.- Axum (das Hyper verwendet, das auf Tokio basiert) nimmt diese
Future
s und plant sie auf seiner Tokio-Laufzeitumgebung. - Innerhalb von
hello_world
undgreet_user
pausierttokio::time::sleep().await
die Ausführung des aktuellen Request-Handler-Future
, ohne den Thread zu blockieren. Dadurch kann der Server gleichzeitig andere eingehende Anfragen bearbeiten. - Die Zeile
axum::Server::bind().serve().await
führt das Hauptserver-Future
bis zur Fertigstellung aus. DiesesFuture
lauscht kontinuierlich auf eingehende Verbindungen und erstellt für jede Anfrage neue Tasks (dieFuture
s sind), die alle von der Tokio-Laufzeitumgebung verwaltet werden.
Diese Konfiguration stellt sicher, dass selbst wenn ein Request-Handler eine lang andauernde asynchrone Operation durchführt (wie das Abrufen von Daten aus einer Datenbank oder einer anderen API), der Server für andere Anfragen reaktionsfähig bleibt.
Fazit
Rusts asynchrone Programmiermodell, das auf dem Future
-Trait aufbaut, von der Tokio-Laufzeitumgebung angetrieben wird und durch async/await
ergonomisch gestaltet ist, bietet eine robuste und effiziente Grundlage für die moderne Webentwicklung. Durch das Verständnis, wie Future
s asynchrone Berechnungen darstellen, wie Tokio sie ausführt und wie async/await
ihre Erstellung und Komposition vereinfacht, können Entwickler Rusts einzigartige Mischung aus Leistung und Sicherheit nutzen, um hochgradig nebenläufige und skalierbare Webdienste zu erstellen. Diese leistungsstarke Kombination erschließt das volle Potenzial von Rust für stark nachgefragte vernetzte Anwendungen und ermöglicht komplexe Logik, ohne Kompromisse bei der Ressourceneffizienz oder der Entwicklererfahrung einzugehen. Wählen Sie Rust für Ihr nächstes asynchrones Webprojekt, um schnelle, zuverlässige und skalierbare Dienste mit Zuversicht zu erstellen.