Async/Await in Rust verstehen
James Reed
Infrastructure Engineer · Leapcell

Async/.await in Rust Asynchrone Programmierung
async/.await
ist eine in Rust eingebaute Sprachfunktion, die es uns ermöglicht, asynchronen Code in einem synchronen Stil zu schreiben.
Lernen wir anhand von Beispielen, wie man die Schlüsselwörter async/.await
verwendet. Bevor wir beginnen, müssen wir das Paket futures
vorstellen. Bearbeiten Sie die Datei Cargo.toml
und fügen Sie den folgenden Inhalt hinzu:
[dependencies] futures = "0.3"
Verwenden von async
zum Erstellen einer asynchronen Future
Einfach ausgedrückt kann das Schlüsselwort async
verwendet werden, um die folgenden Arten von Future
zu erstellen:
- Eine Funktion definieren:
async fn
- Einen Block definieren:
async {}
Zum Beispiel eine async
-Funktion:
async fn hello_world() { ... }
Das Schlüsselwort async
modifiziert den Funktionsprototyp so, dass er ein Future
-Trait-Objekt zurückgibt. Es kapselt dann das Ausführungsergebnis in ein neues Future
und gibt es zurück, was ungefähr dem Folgenden entspricht:
fn hello_world() -> impl Future<Output = ()> { async { ... } }
Hinweis: Der
async
-Block implementiert ein anonymesFuture
-Trait-Objekt, das einenGenerator
einkapselt, welcher einFuture
implementierender Generator ist. EinGenerator
fungiert im Wesentlichen als Zustandsautomat. Wenn eine Operation innerhalb einesasync
-BlocksPoll::Pending
zurückgibt, ruft der Generatoryield
auf und gibt die Ausführung ab. Nach der Wiederaufnahme setzt der Generator die Ausführung fort, bis der gesamte Code abgeschlossen ist, was bedeutet, dass der Zustandsautomat in den ZustandComplete
eintritt undPoll::Ready
zurückgibt, was signalisiert, dass die Ausführung desFuture
abgeschlossen ist.
Ein mit async
markierter Codeblock wird in einen Zustandsautomaten umgewandelt, der das Future
-Trait implementiert. Im Gegensatz zu synchronen Aufrufen, die den aktuellen Thread blockieren, gibt ein Future
, wenn er auf eine blockierende Operation trifft, die Kontrolle über den aktuellen Thread ab und wartet auf das Ausführungsergebnis anderer Future
s.
Ein Future
muss auf einem Executor laufen. Zum Beispiel ist block_on
ein Executor, der den aktuellen Thread blockiert:
// block_on blockiert den aktuellen Thread, bis die angegebene Future die Ausführung abgeschlossen hat. // Dieser Ansatz ist einfach und direkt, aber andere Runtime-Executors bieten komplexere Verhaltensweisen, // wie z.B. die Verwendung von join, um mehrere Futures auf demselben Thread zu planen. use futures::executor::block_on; async fn hello_world() { println!("hello, world!"); } fn main() { let future = hello_world(); // Gibt eine Future zurück, daher wird noch keine Ausgabe gedruckt block_on(future); // Führt die Future aus und wartet auf deren Abschluss; dann wird "hello, world!" gedruckt }
Verwenden von await
zum Warten auf den Abschluss einer anderen asynchronen Future
In der obigen main
-Funktion haben wir den block_on
-Executor verwendet, um auf den Abschluss der Future
zu warten, wodurch der Code synchron erscheint. Aber was ist, wenn Sie eine async fn
innerhalb einer anderen async fn
aufrufen und auf deren Abschluss warten müssen, bevor Sie nachfolgenden Code ausführen? Zum Beispiel:
use futures::executor::block_on; async fn hello_world() { // Direkter Aufruf einer anderen Async-Funktion innerhalb einer Async-Funktion - wird das funktionieren? hello_cat(); println!("hello, world!"); } async fn hello_cat() { println!("hello, kitty!"); } fn main() { let future = hello_world(); block_on(future); }
Hier rufen wir in der async-Funktion hello_world
zuerst eine andere Async-Funktion hello_cat
auf und geben dann "hello, world!"
aus. Überprüfen wir die Ausgabe:
warning: unused implementer of `futures::Future` that must be used --> src/main.rs:6:5 | 6 | hello_cat(); | ^^^^^^^^^^^^ = note: futures do nothing unless you `.await` or poll them ... hello, world!
Wie erwartet haben wir die Future
in main
mit block_on
ausgeführt, aber die von hello_cat
zurückgegebene Future
wurde nie ausgeführt. Glücklicherweise gibt der Compiler eine freundliche Warnung aus: "Futures do nothing unless you .await
or poll them."
Es gibt zwei Lösungen:
- Verwenden Sie die
.await
-Syntax. - Fragen Sie die
Future
manuell ab (was komplexer ist, daher werden wir es hier nicht behandeln).
Ändern wir den Code mit .await
:
use futures::executor::block_on; async fn hello_world() { hello_cat().await; println!("hello, world!"); } async fn hello_cat() { println!("hello, kitty!"); } fn main() { let future = hello_world(); block_on(future); }
Nach dem Hinzufügen von .await
zu hello_cat()
ändert sich die Ausgabe erheblich:
hello, kitty! hello, world!
Die Ausgabereihenfolge folgt nun strikt der Code-Reihenfolge. Dies bedeutet, dass wir eine asynchrone Ausführung erreicht haben, während wir einen sequenziellen Codierungsstil beibehalten haben. Dieser Ansatz ist einfach, effizient und eliminiert Callback-Höllen.
Intern wirkt jedes .await
wie ein Executor, der den Future
-Zustand wiederholt abfragt. Wenn es Pending
zurückgibt, ruft es yield
auf. Andernfalls beendet es die Schleife und schließt die Future
-Ausführung ab. Die Logik ist ungefähr wie folgt:
loop { match some_future.poll() { Pending => yield, Ready(x) => break } }
Kurz gesagt, die Verwendung von .await
innerhalb einer async fn
ermöglicht das Warten auf den Abschluss eines anderen asynchronen Aufrufs. Anders als block_on
blockiert .await
jedoch nicht den aktuellen Thread. Stattdessen wartet er asynchron auf den Abschluss von Future A
. Während des Wartens kann der Thread die Ausführung anderer Future B
-Instanzen fortsetzen, wodurch die Parallelität ermöglicht wird.
Ein Beispiel
Betrachten Sie ein Szenario von Singen und Tanzen. Ohne .await
könnte die Implementierung wie folgt aussehen:
use futures::executor::block_on; struct Song { author: String, name: String, } async fn learn_song() -> Song { Song { author: "Rick Astley".to_string(), name: String::from("Never Gonna Give You Up"), } } async fn sing_song(song: Song) { println!( "Performing {}'s {} ~ {}", song.author, song.name, "Never gonna let you down" ); } async fn dance() { println!("Dancing along to the song"); } fn main() { let song = block_on(learn_song()); // Erster blockierender Aufruf block_on(sing_song(song)); // Zweiter blockierender Aufruf block_on(dance()); // Dritter blockierender Aufruf }
Dieser Code läuft korrekt, erfordert aber drei aufeinanderfolgende blockierende Aufrufe, wobei jeweils eine Aufgabe erledigt wird. In Wirklichkeit könnten wir gleichzeitig singen und tanzen:
use futures::executor::block_on; struct Song { author: String, name: String, } async fn learn_song() -> Song { Song { author: "Rick Astley".to_string(), name: String::from("Never Gonna Give You Up"), } } async fn sing_song(song: Song) { println!( "Performing {}'s {} ~ {}", song.author, song.name, "Never gonna let you down" ); } async fn dance() { println!("Dancing along to the song"); } async fn learn_and_sing() { let song = learn_song().await; sing_song(song).await; } async fn async_main() { let f1 = learn_and_sing(); let f2 = dance(); // Das join!-Makro führt mehrere Futures gleichzeitig aus futures::join!(f1, f2); } fn main() { block_on(async_main()); }
Hier haben Lernen und Singen eine strikte Reihenfolge, aber beides kann mit Tanzen koexistieren. Ohne .await
würde block_on(learn_song())
den aktuellen Thread blockieren und alle anderen Aufgaben, einschließlich Tanzen, verhindern.
Daher ist .await
entscheidend für die asynchrone Programmierung in Rust. Es ermöglicht die gleichzeitige Ausführung mehrerer Aufgaben auf demselben Thread, anstatt sie sequenziell auszuführen.
Fazit
async/.await
ist Rusts eingebautes Werkzeug zum Schreiben asynchroner Funktionen, die wie synchroner Code aussehen. async
konvertiert einen Codeblock in einen Zustandsautomaten, der das Future
-Trait implementiert, das auf einem Executor laufen muss. Anstatt einen ganzen Thread zu blockieren, gibt ein Future
die Kontrolle ab, sodass andere Future
s ausgeführt werden können.
Wesentliche Erkenntnisse:
Future
repräsentiert eine Aufgabe, die in der Zukunft einen Wert liefert.async
erstellt eineFuture
..await
fragt eineFuture
ab und wartet auf deren Abschluss.- Executors (wie
block_on
) verwalten und führenFuture
s aus. - Rusts
async
ist ohne Kosten: keine Heap-Allokation oder dynamischer Versand. - Rust enthält keine integrierte Async-Runtime; Drittanbieterbibliotheken wie
tokio
,async-std
undsmol
stellen diese Funktionalität bereit.
Zusammenfassend lässt sich sagen, dass async/.await
eine effiziente, gleichzeitige Aufgabenausführung in Rust ermöglicht, Callback-Höllen eliminiert und die asynchrone Programmierung intuitiv macht.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Rust-Projekten.
Leapcell ist die Next-Gen Serverless Plattform für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne ungenutzte Gebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollständig automatisierte CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ