Ein genauerer Blick auf die Merkmale für Async

Im Laufe des Kapitels haben wir die Merkmale Future, Pin, Unpin, Stream und StreamExt auf verschiedene Weise verwendet. Bis jetzt haben wir es jedoch vermieden, zu sehr ins Detail zu gehen, wie sie funktionieren oder wie sie zusammenpassen. Wenn wir Rust für den Alltag schreiben, ist das meist ausreichend. Manchmal stößt man jedoch auf Situationen, in denen du weitergehende Details verstehen musst. In diesem Abschnitt werden wir nur so weit ins Detail gehen, wie es für diese Szenarien nötig ist, und überlassen die wirklich tiefen Einblicke der weiteren Dokumentation.

Das Merkmal Future

Lass uns zunächst einen genaueren Blick darauf werfen, wie das Merkmal Future funktioniert. Rust definiert es wie folgt:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

Diese Merkmals-Definition enthält eine Reihe neuer Typen und auch eine Syntax, die wir bisher noch nicht gesehen haben. Gehen wir also die Definition Stück für Stück durch.

Erstens gibt der zugehörige Typ Output von Future an, was das Future zurückgibt. Dies ist analog zum Typ Item des Merkmals Iterator. Zweitens hat Future auch die Methode poll, die eine spezielle Pin-Referenz für ihren self-Parameter und eine veränderbare Referenz auf einen Context-Typ entgegennimmt und Poll<Self::Output> zurückgibt. Wir werden gleich ein wenig mehr über Pin und Context sprechen. Für den Moment wollen wir uns auf das konzentrieren, was die Methode zurückgibt: Den Typ Poll:

#![allow(unused)]
fn main() {
enum Poll<T> {
    Ready(T),
    Pending,
}
}

Dieser Typ Poll ist Option recht ähnlich: Er hat eine Variante Ready(T), die einen Wert hat, und eine Variante Pending ohne Wert. Poll bedeutet jedoch etwas ganz anderes als Option! Die Variante Pending zeigt an, dass das Future noch Arbeit zu erledigen hat, sodass der Aufrufer später noch einmal nachsehen muss. Die Variante Ready zeigt an, dass das Future seine Arbeit beendet hat und der Wert T verfügbar ist.

Hinweis: Bei den meisten Futures sollte der Aufrufer die Methode poll nicht erneut aufrufen, nachdem das Future Ready zurückgegeben hat. Viele Futures werden das Programm zum Absturz bringen, wenn sie erneut abgefragt werden, obwohl sie bereit sind! Futures, bei denen eine erneute Abfrage sicher ist, werden dies in ihrer Dokumentation explizit erwähnen. Dies ist ähnlich zum Verhalten von Iterator::next!

Rust kompiliert Code mit await unter der Haube zu Code, der poll aufruft. Wenn du dir Codeblock 17-4 ansiehst, wo wir den Seitentitel für eine einzelne URL ausgegeben haben, sobald sie aufgelöst wurde, kompiliert Rust das in etwa (wenn auch nicht genau) wie folgt:

match page_title(url).poll() {
    Ready(page_title) => match page_title {
        Some(title) => println!("Der Titel für {url} war {title}"),
        None => println!("{url} hatte keinen Titel"),
    }
    Pending => {
        // Aber was kommt hierhin?
    }
}

Was sollen wir tun, wenn das Future noch Pending ist? Wir brauchen eine Möglichkeit, es nochmal zu versuchen und nochmal und nochmal, bis das Future endlich fertig ist. Mit anderen Worten, wir benötigen eine Schleife:

let mut page_title_fut = page_title(url);
loop {
    match page_title_fut.poll() {
        Ready(value) => match page_title {
            Some(title) => println!("Der Titel für {url} war {title}"),
            None => println!("{url} hatte keinen Titel"),
        }
        Pending => {
            // weitermachen
        }
    }
}

Wenn Rust diesen Code kompilieren würde, würde jedes await blockieren – genau das Gegenteil von dem, was wir erreichen wollten! Stattdessen sorgt Rust dafür, dass die Schleife die Kontrolle an etwas abgeben kann, das die Arbeit an diesem Future unterbrechen und an anderen Futures arbeiten kann, um diese später wieder zu prüfen. Wie wir bereits gesehen haben, ist dieses „Etwas“ eine asynchrone Laufzeitumgebung, und diese Planungs- und Koordinierungsarbeit ist eine der Hauptaufgaben einer Laufzeitumgebung.

Weiter oben in diesem Kapitel haben wir das Warten auf rx.recv beschrieben. Der Aufruf recv gibt ein Future zurück und zum Warten darauf wird es es abgefragt. Wir haben angemerkt, dass eine Laufzeitumgebung das Future pausieren wird, bis es entweder mit Some(message) oder None bereit ist, wenn der Kanal geschlossen wird. Mit unserem tieferen Verständnis des Merkmals Future und insbesondere von Future::poll können wir sehen, wie das funktioniert. Die Laufzeitumgebung weiß, dass das Future nicht bereit ist, wenn es Poll::Pending zurückgibt. Umgekehrt weiß die Laufzeitumgebung, dass das Future bereit ist und bevorzugt es, wenn poll den Wert Poll::Ready(Some(message)) oder Poll::Ready(None) zurückgibt.

Die genauen Details, wie eine Laufzeitumgebung das macht, gehen über den Rahmen dieses Buches hinaus, aber der Schlüssel ist, die grundlegende Mechanik von Futures zu verstehen: Eine Laufzeitumgebung fragt jedes Future ab, für das sie verantwortlich ist, und versetzt das Future zurück in den Schlaf, wenn es noch nicht bereit ist.

Die Merkmale Pin and Unpin

Als wir in Codeblock 17-16 die Idee des Anheftens einführten, stießen wir auf eine sehr unangenehme Fehlermeldung. Hier ist noch einmal der relevante Teil davon:

error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned
  --> src/main.rs:48:33
   |
48 |         trpl::join_all(futures).await;
   |                                 ^^^^^ the trait `Unpin` is not implemented for `{async block@src/main.rs:10:23: 10:33}`, which is required by `Box<{async block@src/main.rs:10:23: 10:33}>: Future`
   |
   = note: consider using the `pin!` macro
           consider using `Box::pin` if you need to access the pinned value outside of the current scope
   = note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
  --> file:///home/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-util-0.3.30/src/future/join_all.rs:29:8
   |
27 | pub struct JoinAll<F>
   |            ------- required by a bound in this struct
28 | where
29 |     F: Future,
   |        ^^^^^^ required by this bound in `JoinAll`

Diese Fehlermeldung sagt uns nicht nur, dass wir die Werte anheften müssen, sondern auch, warum das Anheften erforderlich ist. Die Funktion trpl::join_all gibt eine Struktur namens JoinAll zurück. Diese Struktur ist generisch über einen Typ F, der auf die Implementierung des Merkmals Future beschränkt ist. Direktes Warten auf ein Future mit await heftet das Future implizit an. Deshalb müssen wir pin! nicht überall verwenden, wo wir auf Futures warten wollen.

Allerdings warten wir hier nicht direkt auf ein Future. Stattdessen konstruieren wir ein neues Future JoinAll, indem wir eine Kollektion von Futures an die Funktion join_all übergeben. Die Signatur für join_all erfordert, dass der Typ der Elemente in der Kollektion das Merkmal Future implementiert. Box<T> implementiert Future nur, wenn das T, das es umhüllt, ein Future ist, das das Merkmal Unpin implementiert.

Das ist eine Menge, die man verarbeiten muss! Um es wirklich zu verstehen, müssen wir ein wenig tiefer in die Funktionsweise des Merkmals Future eintauchen, insbesondere in Bezug auf das Anheften (pinning).

Schau dir noch einmal die Definition des Merkmals Future an:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    // Erforderliche Methode
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

Der Parameter cx und sein Typ Context sind der Schlüssel dazu, wie eine Laufzeitumgebung tatsächlich weiß, wann sie ein bestimmtes Future prüfen muss, während es immer noch faul ist. Die Details, wie das funktioniert, liegen jedoch außerhalb des Rahmens dieses Kapitels: Du musst dich im Allgemeinen nur darum kümmern, wenn du eine eigene Future-Implementierung schreibst. Wir werden uns stattdessen auf den Typ von self konzentrieren, da dies das erste Mal ist, dass wir eine Methode sehen, bei der self eine Typ-Annotation hat. Eine Typ-Annotation für self funktioniert wie Typ-Annotationen für andere Funktionsparameter, jedoch mit zwei wesentlichen Unterschieden:

  • Sie teilt Rust mit, welchen Typ self haben muss, damit die Methode aufgerufen werden kann.

  • Sie kann nicht einfach irgendein Typ sein. Sie ist beschränkt auf den Typ, auf dem die Methode implementiert ist, eine Referenz oder ein intelligenter Zeiger auf diesen Typ oder ein Pin, das eine Referenz auf diesen Typ umhüllt.

Wir werden mehr über diese Syntax in Kapitel 18 erfahren. Für den Moment reicht es zu wissen, dass wir, wenn wir ein Future abfragen wollen, um zu prüfen, ob es Pending oder Ready(Output) ist, eine mit Pin umhüllte veränderbare Referenz auf den Typ benötigen.

Pin ist ein Wrapper für zeigerartige Typen wie &, &mut, Box und Rc. (Technisch gesehen arbeitet Pin mit Typen, die die Merkmale Deref oder DerefMut implementieren, aber das ist effektiv gleichbedeutend damit, nur mit Zeigern zu arbeiten.) Pin ist selbst kein Zeiger und hat kein eigenes Verhalten wie Rc und Arc mit Referenzzählern; es ist lediglich ein Werkzeug, das der Compiler verwenden kann, um Einschränkungen bei der Verwendung von Zeigern zu erzwingen.

Wenn du dich daran erinnerst, dass await in Form von Aufrufen von poll implementiert ist, erklärt das die Fehlermeldung, die wir oben gesehen haben, aber die bezog sich auf Unpin, nicht auf Pin. Wie genau verhält sich also Pin zu Unpin, und warum muss self bei einem Future in einem Pin-Typ sein, um poll aufzurufen?

Erinnere dich an den Anfang dieses Kapitels: Eine Reihe von await-Punkten in einem Future wird zu einem Zustandsautomaten kompiliert, und der Compiler stellt sicher, dass dieser Zustandsautomat alle normalen Sicherheitsregeln von Rust befolgt, einschließlich Ausleihen (borrowing) und Eigentümerschaft (ownership). Damit das funktioniert, prüft Rust, welche Daten zwischen einem await-Punkt und entweder dem nächsten await-Punkt oder dem Ende des asynchronen Blocks benötigt werden. Anschließend wird eine entsprechende Variante in der kompilierten Zustandsmaschine erstellt. Jede Variante erhält den erforderlichen Zugriff auf die Daten, die in diesem Abschnitt des Quellcodes verwendet werden, entweder durch Übernahme der Eigentümerschaft an diesen Daten oder durch Erhalt einer veränderbaren oder unveränderbaren Referenz darauf.

So weit, so gut: Wenn wir bei der Eigentümerschaft oder den Referenzen in einem bestimmten asynchronen Block etwas falsch machen, wird uns der Ausleihenprüfer (borrow checker) dies mitteilen. Wenn wir das Future, das diesem Block entspricht, verschieben wollen – etwa in einen Vec, um es an join_all zu übergeben – wird es schwieriger.

Wenn wir ein Future verschieben – sei es durch Verschieben in eine Datenstruktur, um es als Iterator mit join_all zu verwenden oder durch Rückgabe aus einer Funktion – bedeutet das eigentlich, dass wir die Zustandsmaschine verschieben, die Rust für uns erstellt. Und im Gegensatz zu den meisten anderen Typen in Rust können die Futures, die Rust für async-Blöcke erzeugt, mit Referenzen auf sich selbst in den Feldern einer beliebigen Variante enden, wie in der vereinfachten Darstellung in Abbildung 17-4 gezeigt.

Nebenläufiger Arbeitsablauf

Abbildung 17-4: Ein selbstreferenzierender Datentyp

Standardmäßig kann ein Objekt, das eine Referenz auf sich selbst hat, nicht sicher verschoben werden, da Referenzen immer auf die tatsächliche Speicheradresse des Objekts zeigen (siehe Abbildung 17-5). Wenn du die Datenstruktur selbst verschiebst, verweisen diese internen Referenzen weiterhin auf den alten Speicherplatz. Dieser Speicherplatz ist nun jedoch ungültig. Zum einen wird ihr Wert nicht mehr aktualisiert, wenn du Änderungen an der Datenstruktur vornimmst. Zum anderen – und das ist noch wichtiger – kann der Computer diesen Speicherplatz nun für andere Zwecke verwenden! Es könnte sein, dass du später völlig unzusammenhängende Daten liest.

Nebenläufiger Arbeitsablauf

Abbildung 17-5: Das unsichere Ergebnis beim Verschieben eines selbstreferenzierenden Datentyps

Theoretisch könnte der Rust-Compiler versuchen, jede Referenz auf ein Objekt zu aktualisieren, wenn es verschoben wird. Das würde potenziell eine Menge zusätzlicher Performance-Overhead bedeuten, vor allem wenn man bedenkt, dass es ein ganzes Netz von Referenzen geben kann, die aktualisiert werden müssen. Wenn wir stattdessen sicherstellen können, dass die betreffende Datenstruktur nicht im Speicher verschoben wird, müssen wir keine Referenzen aktualisieren. Das ist genau das, was der Rust-Ausleihenprüfer verlangt: In sicherem Code kann man kein ein Element, auf das aktive Referenzen bestehen, verschieben.

Pin baut darauf auf, um uns genau die Garantie zu geben, die wir brauchen. Wenn wir einen Wert anheften, indem wir einen Zeiger auf diesen Wert in Pin einpacken, kann er nicht mehr verschoben werden. Wenn du also Pin<Box<SomeType>> hast, heftest du eigentlich den Wert SomeType an, nicht den Zeiger Box. Abbildung 17-6 veranschaulicht dies:

Nebenläufiger Arbeitsablauf

Abbildung 17-6: Anheften einer Box, die auf einen selbstreferenzierenden Future-Typ zeigt

In der Tat kann der Zeiger in Box immer noch verschoben werden. Denke daran: Wir wollen sicherstellen, dass die Daten, auf die letztlich referenziert wird, an ihrem Platz bleiben. Wenn ein Zeiger verschoben wird, aber die Daten, auf die er zeigt, an der gleichen Stelle sind, wie in Abbildung 17-7, gibt es kein potenzielles Problem. (Schau dir als unabhängige Übung die Dokumentationen der Typen sowie des Moduls std::pin an und versuche herauszufinden, wie du das mit einem Pin machst, der eine Box umhüllt.) Der Schlüssel ist, dass der selbstreferenzierende Typ selbst nicht verschoben werden kann, weil er immer noch angeheftet ist.

Nebenläufiger Arbeitsablauf

Abbildung 17-7: Verschieben einer Box, die auf einen selbstreferenzierenden Futuretyp zeigt.

Die meisten Typen können jedoch gefahrlos verschoben werden, selbst wenn sie sich hinter einem Pin-Wrapper befinden. Wir müssen nur über das Anheften nachdenken, wenn Elemente interne Referenzen haben. Primitive Werte wie Zahlen und Boolesche Werte sind sicher, weil sie keine internen Referenzen haben. Genauso wenig wie die meisten Typen, mit denen man normalerweise in Rust arbeitet. Du kannst zum Beispiel unbesorgt einen Vec verschieben. Nach dem, was wir bisher gesehen haben, müsste man bei einem Pin<Vec<String>> alles über die sicheren, aber restriktiven APIs von Pin machen, obwohl ein Vec<String> immer sicher verschoben werden kann, wenn es keine anderen Referenzen auf ihn gibt. Wir brauchen eine Möglichkeit, dem Compiler mitzuteilen, dass es in solchen Fällen in Ordnung ist, Elemente zu verschieben – und hier kommt Unpin ins Spiel.

Unpin ist ein Markierungsmerkmal (marker trait), ähnlich wie die Merkmale Send und Sync, die wir in Kapitel 16 gesehen haben, und es hat keine eigene Funktionalität. Markierungsmerkmale existieren nur, um dem Compiler mitzuteilen, dass es sicher ist, den Typ zu verwenden, der ein bestimmtes Merkmal in einem bestimmten Kontext implementiert. Unpin teilt dem Compiler mit, dass ein gegebener Typ keine besonderen Garantien aufrechterhalten muss, um den fraglichen Wert zu verschieben.

Genau wie bei Send und Sync implementiert der Compiler Unpin automatisch für alle Typen, bei denen er beweisen kann, dass sie sicher sind. Ein Sonderfall analog zu Send und Sync ist, dass Unpin für einen Typ nicht implementiert ist. Die Notation hierfür ist impl !Unpin for SomeType, wobei SomeType der Name eines Typs ist, der diese Garantien aufrechterhalten muss, um sicher zu sein, wenn ein Zeiger auf diesen Typ in einem Pin verwendet wird.

Mit anderen Worten, es gibt zwei Dinge über die Beziehung zwischen Pin und Unpin zu beachten. Erstens ist Unpin der „normale“ Fall und !Unpin der Spezialfall. Zweitens, ob ein Typ Unpin oder !Unpin implementiert, spielt nur eine Rolle, wenn man einen angepinnten Zeiger auf diesen Typ wie Pin<&mut SomeType> verwendet.

Um dies zu verdeutlichen, denke an einen String: Er hat eine Länge und die Unicode-Zeichen, aus denen er besteht. Wir können einen String in einen Pin einpacken, wie in Abbildung 17-8. Allerdings implementiert String automatisch Unpin, wie die meisten anderen Typen in Rust.

Concurrent work flow

Abbildung 17-8: Anheften eines String; die gestrichelte Linie deutet an, dass die Zeichenkette das Merkmal Unpin implementiert und daher nicht angeheftet ist.

Infolgedessen können wir Dinge tun, die illegal wären, wenn String stattdessen !Unpin implementiert hätte, wie zum Beispiel das Ersetzen einer Zeichenkette durch eine andere an der exakt gleichen Stelle im Speicher, wie in Abbildung 17-9. Dies verletzt nicht den Pin-Vertrag, weil String keine internen Referenzen hat, die es unsicher machen, es zu verschieben! Das ist genau der Grund, warum es Unpin und nicht !Unpin implementiert.

Concurrent work flow

Abbildung 17-9: Ersetzen eines String durch einen völlig anderen String im Speicher.

Jetzt wissen wir genug, um die Fehler zu verstehen, die für den Aufruf join_all in Codeblock 17-17 gemeldet wurden. Ursprünglich haben wir versucht, die von asynchronen Blöcken erzeugten Futures in einen Vec<Box<dyn Future<Output = ()>>> zu verschieben, aber wie wir gesehen haben, können diese Futures interne Referenzen haben, sodass sie Unpin nicht implementieren. Sie müssen angepinnt werden und dann können wir den Typ Pin an den Vec übergeben, in der Gewissheit, dass die zugrunde liegenden Daten in den Futures nicht verschoben werden.

Pin und Unpin sind vor allem wichtig für die Erstellung von Low-Level-Bibliotheken und wenn du eine Laufzeitumgebung erstellst, weniger bei alltäglichem Rust-Code. Wenn du diese Merkmale in Fehlermeldungen siehst, hast du jetzt eine bessere Vorstellung davon, wie du deinen Code korrigieren kannst!

Anmerkung: Diese Kombination von Pin und Unpin macht es möglich, eine ganze Klasse von komplexen Typen sicher in Rust zu implementieren, die sich sonst als schwierig erweisen würden, weil sie selbstreferenzierend sind. Typen, die Pin benötigen, tauchen heute am häufigsten in asynchronem Rust auf, aber hin und wieder sieht man sie auch in anderen Kontexten.

Die Besonderheiten der Funktionsweise von Pin und Unpin und die Regeln, die sie einhalten müssen, werden ausführlich in der API-Dokumentation für std::pin behandelt. Wenn du mehr darüber lernen willst, ist das ein guter Ausgangspunkt.

Wenn du noch detaillierter verstehen willst, wie die Dinge unter der Haube funktionieren, schaue dir die Kapitel „Under the Hood: Executing Futures and Tasks“ und „Pinning“ im Buch Asynchronous Programming in Rust an:

Das Merkmal Stream

Nachdem du nun ein tieferes Verständnis für die Merkmale Future, Pin und Unpin hast, können wir uns dem Merkmal Stream zuwenden. Wie du bereits in diesem Kapitel gelernt hast, sind Ströme ähnlich wie asynchrone Iteratoren. Im Gegensatz zu Iterator und Future hat Stream derzeit keine Definition in der Standardbibliothek, aber es gibt eine sehr verbreitete Definition in der Kiste Futures, die im gesamten Ökosystem verwendet wird.

Schauen wir uns die Definitionen der Merkmale Iterator und Future an, bevor wir uns ansehen, wie ein Merkmal Stream aussehen könnte. Von Iterator haben wir die Idee einer Sequenz: Seine Methode next liefert eine Option<Self::Item>. Von Future haben wir die Idee der zeitlichen Bereitschaft: Seine Methode poll liefert ein Poll<Self::Output>. Um eine Sequenz von Elementen darzustellen, die im Laufe der Zeit bereit sein werden, definieren wir ein Merkmal Stream, das diese Funktionalitäten zusammenführt:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;

    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}
}

Das Merkmal Stream definiert einen zugehörigen Typ namens Item für den Typ der vom Strom erzeugten Elemente. Dies ist ähnlich wie bei Iterator, wo es beliebig viele Elemente geben kann, anders als bei Future, wo es immer nur einen einzigen Output gibt, selbst wenn es der Einheitstyp () ist.

Stream definiert auch eine Methode zum Abrufen dieser Elemente. Wir nennen sie poll_next, um zu verdeutlichen, dass sie auf die gleiche Weise wie Future::poll abfragt und eine Sequenz von Elementen auf die gleiche Weise wie Iterator::next erzeugt. Sein Rückgabetyp kombiniert Poll mit Option. Der äußere Typ ist Poll, weil er auf Bereitschaft geprüft werden muss, genau wie ein Future. Der innere Typ ist Option, weil er signalisieren muss, ob es weitere Nachrichten gibt, genau wie ein Iterator.

Etwas, das dieser Definition sehr ähnlich ist, wird wahrscheinlich Teil der Standardbibliothek von Rust werden. In der Zwischenzeit ist es Teil des Werkzeugkoffers der meisten Laufzeitumgebungen, sodass du dich darauf verlassen kannst, und alles, was wir als nächstes behandeln, allgemein gilt!

Im Beispiel, das wir im Abschnitt über Ströme gesehen haben, haben wir allerdings nicht poll_next oder Stream benutzt, sondern next und StreamExt. Wir könnten direkt mit der poll_next-API arbeiten, indem wir unsere eigenen Stream-Zustandsautomaten schreiben, genauso wie wir mit Futures direkt über deren Methode poll arbeiten können. Die Verwendung von await ist jedoch viel schöner, und das Merkmal StreamExt stellt die Methode next bereit, sodass wir folgendes tun können:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;
    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Self::Item>>;
}

trait StreamExt: Stream {
    async fn next(&mut self) -> Option<Self::Item>
    where
        Self: Unpin;

    // andere Methoden ...
}
}

Anmerkung: Die tatsächliche Definition von StreamExt sieht etwas anders aus, da sie Versionen von Rust unterstützt, die noch keine Verwendung von asynchronen Funktionen in Merkmalen kennen. Infolgedessen sieht sie so aus:

fn next(&mut self) -> Next<'_, Self> where Self: Unpin;

Der Typ Next ist ein struct, das Future implementiert, und erlaubt uns, die Lebensdauer der Referenz auf self mit Next<'_, Self> zu benennen, sodass await mit dieser Methode arbeiten kann!

Das Merkmal StreamExt ist auch die Heimat aller interessanten Methoden, die für die Verwendung mit Strömen zur Verfügung stehen. StreamExt wird automatisch für jeden Typ implementiert, der Stream implementiert, aber diese Merkmale werden separat definiert, um der Rust-Gemeinschaft die Möglichkeit zu geben, Komfort-APIs zu entwickeln, ohne die grundlegenden Merkmale zu beeinflussen.

In der Version von StreamExt, die in der Kiste trpl verwendet wird, definiert das Merkmal nicht nur die Methode next, sondern liefert auch eine Implementierung von next, die die Details des Aufrufs von Stream::poll_next korrekt behandelt. Das bedeutet, dass du selbst beim Schreiben deines eigenen Streaming-Datentyps nur Stream implementieren musst, und dann kann jeder, der deinen Datentyp verwendet, StreamExt und seine Methoden automatisch mit ihm verwenden.

Das ist alles, was wir für die tieferen Details zu diesen Merkmalen behandeln werden. Zum Abschluss wollen wir uns ansehen, wie Futures (einschließlich Ströme), Aufgaben und Stränge zusammenpassen!