Bliższe spojrzenie na cechy dla Async
W całym rozdziale używaliśmy cech Future, Stream i StreamExt na różne sposoby. Jak dotąd unikaliśmy jednak zagłębiania się w szczegóły ich działania lub ich wzajemnego dopasowania, co w większości przypadków jest w porządku w codziennej pracy z Rust. Czasami jednak napotkasz sytuacje, w których będziesz musiał zrozumieć kilka więcej szczegółów tych cech, a także typ Pin i cechę Unpin. W tej sekcji zagłębimy się w nie na tyle, aby pomóc w takich scenariuszach, pozostawiając naprawdę dogłębne badanie innym dokumentacjom.
Cechy Future
Zacznijmy od bliższego przyjrzenia się, jak działa cecha Future. Oto jak Rust ją definiuje:
#![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>;
}
}
Ta definicja cechy zawiera wiele nowych typów, a także pewną składnię, której wcześniej nie widzieliśmy, więc przejdźmy przez definicję kawałek po kawałku.
Po pierwsze, typ skojarzony Output z cechy Future mówi, do czego future się rozwiązuje. Jest to analogiczne do typu skojarzonego Item z cechy Iterator. Po drugie, cecha Future ma metodę poll, która przyjmuje specjalną referencję Pin dla swojego parametru self oraz mutowalną referencję do typu Context i zwraca Poll<Self::Output>. Więcej o Pin i Context powiemy za chwilę. Na razie skupmy się na tym, co zwraca metoda, czyli na typie Poll:
#![allow(unused)]
fn main() {
pub enum Poll<T> {
Ready(T),
Pending,
}
}
Ten typ Poll jest podobny do Option. Ma jeden wariant, który ma wartość, Ready(T), i jeden, który nie ma, Pending. Poll oznacza jednak coś zupełnie innego niż Option! Wariant Pending wskazuje, że future nadal ma pracę do wykonania, więc wywołujący będzie musiał sprawdzić ponownie później. Wariant Ready wskazuje, że Future zakończyło swoją pracę i wartość T jest dostępna.
Uwaga: Rzadko zdarza się potrzeba bezpośredniego wywołania
poll, ale jeśli zajdzie taka potrzeba, pamiętaj, że w przypadku większości futures, wywołujący nie powinien ponownie wywoływaćpollpo tym, jak future zwróciłoReady. Wiele futures panikuje, jeśli zostanie ponownie odpytanych po staniu się gotowymi. Futures, które są bezpieczne do ponownego odpytania, będą o tym wyraźnie informować w swojej dokumentacji. Jest to podobne do zachowaniaIterator::next.
Kiedy widzisz kod, który używa await, Rust kompiluje go pod spodem do kodu, który wywołuje poll. Jeśli spojrzysz ponownie na Listing 17-4, gdzie wydrukowaliśmy tytuł strony dla pojedynczego adresu URL po jego rozwiązaniu, Rust kompiluje go do czegoś w rodzaju (choć nie dokładnie) tego:
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// But what goes here?
}
}
Co powinniśmy zrobić, gdy future jest nadal w stanie Pending? Potrzebujemy jakiegoś sposobu, aby spróbować ponownie, i ponownie, i ponownie, aż future będzie w końcu gotowe. Innymi słowy, potrzebujemy pętli:
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// continue
}
}
}
Jednak gdyby Rust skompilował to dokładnie na taki kod, każde await byłoby blokujące – dokładnie przeciwnie do tego, co zamierzaliśmy! Zamiast tego Rust zapewnia, że pętla może przekazać kontrolę czemuś, co może wstrzymać pracę nad tym future, aby pracować nad innymi futures, a następnie sprawdzić ten ponownie później. Jak widzieliśmy, tym czymś jest środowisko uruchomieniowe async, a ta praca związana z planowaniem i koordynacją jest jednym z jego głównych zadań.
W sekcji „Wysyłanie danych między dwoma zadaniami za pomocą przekazywania wiadomości” opisaliśmy oczekiwanie na rx.recv. Wywołanie recv zwraca future, a oczekiwanie na future odpytuje je. Zauważyliśmy, że środowisko uruchomieniowe wstrzyma future, dopóki nie będzie ono gotowe z Some(message) lub None, gdy kanał zostanie zamknięty. Dzięki naszemu głębszemu zrozumieniu cechy Future, a konkretnie Future::poll, możemy zobaczyć, jak to działa. Środowisko uruchomieniowe wie, że future nie jest gotowe, gdy zwraca Poll::Pending. Odwrotnie, środowisko uruchomieniowe wie, że future jest gotowe i kontynuuje je, gdy poll zwraca Poll::Ready(Some(message)) lub Poll::Ready(None).
Dokładne szczegóły tego, jak środowisko wykonawcze to robi, wykraczają poza zakres tej książki, ale kluczem jest zrozumienie podstawowych mechanizmów futures: środowisko wykonawcze odpytuje każdy future, za który jest odpowiedzialne, usypiając future ponownie, gdy nie jest jeszcze gotowe.
Typ Pin i cecha Unpin
W Listing 17-13 użyliśmy makra trpl::join! do oczekiwania na trzy futures. Jednak często zdarza się mieć kolekcję, taką jak wektor, zawierającą pewną liczbę futures, której nie będzie znana do czasu wykonania. Zmieńmy Listing 17-13 na kod z Listing 17-23, który umieszcza trzy futures w wektorze i wywołuje funkcję trpl::join_all zamiast tego, co jeszcze się nie skompiluje.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
Każdy future umieszczamy w Box, aby zamienić je w obiekty cech, tak jak to zrobiliśmy w sekcji „Zwracanie błędów z run” w Rozdziale 12. (Obiekty cech szczegółowo omówimy w Rozdziale 18.) Używanie obiektów cech pozwala nam traktować każdy z anonimowych futures wyprodukowanych przez te typy jako ten sam typ, ponieważ wszystkie one implementują cechę Future.
Może to być zaskakujące. Przecież żaden z bloków async niczego nie zwraca, więc każdy z nich produkuje Future<Output = ()>. Pamiętaj jednak, że Future jest cechą, a kompilator tworzy unikalny enum dla każdego bloku async, nawet jeśli mają identyczne typy wyjściowe. Tak jak nie możesz umieścić dwóch różnych, ręcznie napisanych struktur w Vec, tak samo nie możesz mieszać enumów generowanych przez kompilator.
Następnie przekazujemy kolekcję futures do funkcji trpl::join_all i czekamy na wynik. Jednak to się nie kompiluje; oto odpowiednia część komunikatów o błędach.
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
|
= 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<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/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`
Notatka w komunikacie o błędzie mówi nam, że powinniśmy użyć makra pin!, aby przypiąć wartości, co oznacza umieszczenie ich w typie Pin, który gwarantuje, że wartości nie zostaną przeniesione w pamięci. Komunikat o błędzie mówi, że przypinanie jest wymagane, ponieważ dyn Future<Output = ()> musi implementować cechę Unpin, a obecnie tego nie robi.
Funkcja trpl::join_all zwraca strukturę o nazwie JoinAll. Ta struktura jest generyczna na typie F, który jest ograniczony do implementacji cechy Future. Bezpośrednie oczekiwanie na future za pomocą await niejawnie przypina future. Dlatego nie musimy używać pin! wszędzie tam, gdzie chcemy oczekiwać na futures.
Nie oczekujemy tu jednak bezpośrednio na future. Zamiast tego konstruujemy nowe future, JoinAll, przekazując kolekcję futures do funkcji join_all. Sygnatura join_all wymaga, aby typy elementów w kolekcji implementowały cechę Future, a Box<T> implementuje Future tylko wtedy, gdy opakowywany przez niego T jest futurem, który implementuje cechę Unpin.
To dużo do przyswojenia! Aby to naprawdę zrozumieć, zagłębmy się nieco bardziej w to, jak działa cecha Future, szczególnie w kontekście przypinania. Spójrzmy jeszcze raz na definicję cechy Future:
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}
Parametr cx i jego typ Context są kluczem do tego, jak środowisko wykonawcze faktycznie wie, kiedy sprawdzić dany future, jednocześnie pozostając leniwym. Ponownie, szczegóły tego, jak to działa, wykraczają poza zakres tego rozdziału, i zazwyczaj musisz o tym myśleć tylko wtedy, gdy piszesz niestandardową implementację Future. Zamiast tego skupimy się na typie self, ponieważ jest to pierwszy raz, gdy widzieliśmy metodę, w której self ma adnotację typu. Adnotacja typu dla self działa jak adnotacje typu dla innych parametrów funkcji, ale z dwoma kluczowymi różnicami:
- Mówi Rustowi, jakiego typu musi być
self, aby metoda mogła zostać wywołana. - Nie może to być dowolny typ. Jest ograniczony do typu, na którym metoda jest zaimplementowana, referencji lub inteligentnego wskaźnika do tego typu, lub
Pinopakowującego referencję do tego typu.
Więcej na temat tej składni zobaczymy w Rozdziale 18. Na razie wystarczy wiedzieć, że jeśli chcemy odpytać future, aby sprawdzić, czy jest Pending czy Ready(Output), potrzebujemy mutowalnej referencji do typu opakowanej w Pin.
Pin to opakowanie dla typów wskaźnikopodobnych, takich jak &, &mut, Box i Rc. (Technicznie Pin działa z typami implementującymi cechy Deref lub DerefMut, ale to jest skutecznie równoważne z pracą tylko z referencjami i inteligentnymi wskaźnikami.) Pin sam w sobie nie jest wskaźnikiem i nie ma żadnego własnego zachowania, jak Rc i Arc zliczające referencje; jest to czysto narzędzie, którego kompilator może używać do wymuszania ograniczeń na użycie wskaźników.
Przypominając, że await jest implementowane w kategoriach wywołań poll, zaczyna wyjaśniać się komunikat o błędzie, który widzieliśmy wcześniej, ale był on w kategoriach Unpin, a nie Pin. Jak więc dokładnie Pin odnosi się do Unpin i dlaczego Future potrzebuje, aby self było w typie Pin, aby wywołać poll?
Przypomnijmy z wcześniejszej części tego rozdziału, że seria punktów oczekiwania w future jest kompilowana w maszynę stanów, a kompilator dba o to, aby ta maszyna stanów przestrzegała wszystkich normalnych zasad Rusta dotyczących bezpieczeństwa, w tym pożyczania i własności. Aby to działało, Rust patrzy na to, jakie dane są potrzebne między jednym punktem oczekiwania a następnym punktem oczekiwania lub końcem bloku async. Następnie tworzy odpowiedni wariant w skompilowanej maszynie stanów. Każdy wariant uzyskuje potrzebny dostęp do danych, które będą używane w tej sekcji kodu źródłowego, albo poprzez przejęcie własności tych danych, albo poprzez uzyskanie mutowalnej lub niemutowalnej referencji do nich.
Jak dotąd, wszystko dobrze: jeśli popełnimy błąd w kwestii własności lub referencji w danym bloku async, narzędzie borrow checker nas o tym poinformuje. Kiedy chcemy przenosić future odpowiadające temu blokowi – na przykład przenosząc je do Vec, aby przekazać do join_all – sprawy stają się bardziej skomplikowane.
Kiedy przenosimy future – czy to poprzez włożenie go do struktury danych w celu użycia jako iteratora z join_all, czy zwracając je z funkcji – oznacza to faktycznie przeniesienie maszyny stanów, którą Rust dla nas tworzy. I w przeciwieństwie do większości innych typów w Rust, futures, które Rust tworzy dla bloków async, mogą mieć odwołania do samych siebie w polach dowolnego wariantu, jak pokazano na uproszczonej ilustracji na Rysunku 17-4.
Jednakże, domyślnie każdy obiekt, który ma do siebie referencję, jest niebezpieczny do przenoszenia, ponieważ referencje zawsze wskazują na rzeczywisty adres pamięci tego, do czego się odnoszą (patrz Rysunek 17-5). Jeśli przeniesiesz samą strukturę danych, te wewnętrzne referencje będą wskazywać na stare miejsce. Jednak to miejsce w pamięci jest teraz nieprawidłowe. Po pierwsze, jego wartość nie zostanie zaktualizowana, gdy wprowadzisz zmiany w strukturze danych. Po drugie – co ważniejsze – komputer może teraz swobodnie ponownie wykorzystać tę pamięć do innych celów! Później możesz odczytać zupełnie niepowiązane dane.
Teoretycznie, kompilator Rust mógłby próbować aktualizować każdą referencję do obiektu, gdy jest on przenoszony, ale to mogłoby dodać wiele narzutu wydajnościowego, zwłaszcza jeśli cała sieć referencji wymaga aktualizacji. Gdybyśmy zamiast tego mogli zapewnić, że dana struktura danych nie przesuwa się w pamięci, nie musielibyśmy aktualizować żadnych referencji. Do tego właśnie służy borrow checker Rusta: w bezpiecznym kodzie zapobiega przenoszeniu jakiegokolwiek elementu, do którego istnieje aktywna referencja.
Pin opiera się na tym, aby zapewnić nam dokładnie taką gwarancję, jakiej potrzebujemy. Kiedy przypinamy wartość, opakowując wskaźnik do tej wartości w Pin, nie może się ona już przesuwać. Zatem, jeśli masz Pin<Box<SomeType>>, faktycznie przypinasz wartość SomeType, nie wskaźnik Box. Rysunek 17-6 ilustruje ten proces.
W rzeczywistości wskaźnik Box może nadal swobodnie się przemieszczać. Pamiętaj: zależy nam na tym, aby dane, do których ostatecznie się odwołujemy, pozostały na miejscu. Jeśli wskaźnik się przemieszcza, ale dane, na które wskazuje, znajdują się w tym samym miejscu, jak na Rysunku 17-7, nie ma potencjalnego problemu. (Jako niezależne ćwiczenie, spójrz na dokumentację typów oraz modułu std::pin i spróbuj ustalić, jak byś to zrobił z Pin opakowującym Box.) Kluczem jest to, że samoodwołujący się typ sam w sobie nie może się przemieszczać, ponieważ jest nadal przypięty.
Jednak większość typów jest całkowicie bezpieczna do przenoszenia, nawet jeśli znajdują się za wskaźnikiem Pin. Musimy myśleć o przypinaniu tylko wtedy, gdy elementy mają wewnętrzne referencje. Prymitywne wartości, takie jak liczby i wartości logiczne, są bezpieczne, ponieważ oczywiście nie mają żadnych wewnętrznych referencji.
Podobnie jak większość typów, z którymi normalnie pracujesz w Rust. Możesz przenosić Vec, na przykład, bez obaw. Biorąc pod uwagę to, co widzieliśmy do tej pory, jeśli masz Pin<Vec<String>>, musiałbyś robić wszystko za pomocą bezpiecznych, ale restrykcyjnych API dostarczonych przez Pin, mimo że Vec<String> jest zawsze bezpieczny do przenoszenia, jeśli nie ma do niego innych referencji. Potrzebujemy sposobu, aby powiedzieć kompilatorowi, że w takich przypadkach można przenosić elementy – i tu właśnie wchodzi Unpin.
Unpin to cecha znacznikowa, podobna do cech Send i Sync, które widzieliśmy w Rozdziale 16, i dlatego nie ma własnej funkcjonalności. Cechy znacznikowe istnieją tylko po to, aby poinformować kompilator, że bezpieczne jest użycie typu implementującego daną cechę w określonym kontekście. Unpin informuje kompilator, że dany typ nie musi przestrzegać żadnych gwarancji dotyczących tego, czy dana wartość może być bezpiecznie przeniesiona.
Podobnie jak w przypadku Send i Sync, kompilator automatycznie implementuje Unpin dla wszystkich typów, dla których może to udowodnić, że jest bezpieczne. Szczególnym przypadkiem, ponownie podobnym do Send i Sync, jest sytuacja, gdy Unpin nie jest implementowany dla typu. Oznaczenie dla tego to impl !Unpin for SomeType, gdzie SomeType to nazwa typu, który musi przestrzegać tych gwarancji, aby być bezpiecznym za każdym razem, gdy wskaźnik do tego typu jest używany w Pin.
Innymi słowy, istnieją dwie rzeczy, o których należy pamiętać w związku z relacją między Pin a Unpin. Po pierwsze, Unpin jest przypadkiem „normalnym”, a !Unpin jest przypadkiem specjalnym. Po drugie, to, czy typ implementuje Unpin czy !Unpin, ma znaczenie tylko wtedy, gdy używasz przypiętego wskaźnika do tego typu, takiego jak Pin<&mut SomeType>.
Aby to uściślić, pomyśl o String: ma długość i znaki Unicode, które ją tworzą. Możemy opakować String w Pin, jak widać na Rysunku 17-8. Jednak String automatycznie implementuje Unpin, podobnie jak większość innych typów w Rust.
W rezultacie możemy robić rzeczy, które byłyby nielegalne, gdyby String implementował zamiast tego !Unpin, takie jak zastępowanie jednego ciągu znaków innym w dokładnie tym samym miejscu w pamięci, jak na Rysunku 17-9. Nie narusza to kontraktu Pin, ponieważ String nie ma wewnętrznych odwołań, które czyniłyby jego przenoszenie niebezpiecznym. Właśnie dlatego implementuje Unpin, a nie !Unpin.
Teraz wiemy wystarczająco dużo, aby zrozumieć błędy zgłoszone dla wywołania join_all z Listing 17-23. Pierwotnie próbowaliśmy przenieść futures wyprodukowane przez bloki async do Vec<Box<dyn Future<Output = ()>>>, ale jak widzieliśmy, te futures mogą mieć wewnętrzne referencje, więc nie implementują automatycznie Unpin. Po ich przypięciu możemy przekazać wynikowy typ Pin do Vec, ufając, że bazowe dane w futures nie zostaną przeniesione. Listing 17-24 pokazuje, jak naprawić kod, wywołując makro pin! tam, gdzie zdefiniowane są wszystkie trzy futures, i dostosowując typ obiektu cechy.
extern crate trpl; // required for mdbook test
use std::pin::{Pin, pin};
// --snip--
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let rx_fut = pin!(async {
// --snip--
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
let tx_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;
});
}
Ten przykład kompiluje się i działa, a my moglibyśmy dodawać lub usuwać futures z wektora w czasie wykonania i łączyć je wszystkie.
Pin i Unpin są głównie ważne przy tworzeniu bibliotek niższego poziomu lub gdy budujesz samo środowisko wykonawcze, a nie w codziennym kodzie Rust. Kiedy jednak zobaczysz te cechy w komunikatach o błędach, będziesz miał lepszy pomysł, jak naprawić swój kod!
Uwaga: To połączenie
PiniUnpinumożliwia bezpieczną implementację całej klasy złożonych typów w Rust, które w przeciwnym razie okazałyby się trudne, ponieważ są samoreferencyjne. Typy wymagającePinpojawiają się najczęściej w dzisiejszym asynchronicznym Rust, ale co jakiś czas możesz je również zobaczyć w innych kontekstach.Szczegóły dotyczące działania
PiniUnpinoraz zasad, których muszą przestrzegać, są szczegółowo omówione w dokumentacji API dlastd::pin, więc jeśli jesteś zainteresowany pogłębianiem wiedzy, to świetne miejsce, aby zacząć.Jeśli chcesz zrozumieć, jak wszystko działa pod maską jeszcze bardziej szczegółowo, zobacz Rozdziały 2 i 4 z książki Asynchronous Programming in Rust async-book.
Cecha Stream
Teraz, gdy masz głębsze zrozumienie cech Future, Pin i Unpin, możemy zwrócić uwagę na cechę Stream. Jak dowiedziałeś się wcześniej w rozdziale, strumienie są podobne do asynchronicznych iteratorów. Jednak w przeciwieństwie do Iterator i Future, Stream nie ma definicji w standardowej bibliotece w momencie pisania tego tekstu, ale istnieje bardzo powszechna definicja z kraty futures używana w całym ekosystemie.
Przyjrzyjmy się definicjom cech Iterator i Future, zanim zastanowimy się, jak cecha Stream mogłaby je połączyć. Z Iterator mamy ideę sekwencji: jego metoda next dostarcza Option<Self::Item>. Z Future mamy ideę gotowości w czasie: jego metoda poll dostarcza Poll<Self::Output>. Aby reprezentować sekwencję elementów, które stają się gotowe w czasie, definiujemy cechę Stream, która łączy te funkcje:
#![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>>;
}
}
Cecha Stream definiuje typ skojarzony o nazwie Item dla typu elementów wytwarzanych przez strumień. Jest to podobne do Iterator, gdzie może być zero lub wiele elementów, i w przeciwieństwie do Future, gdzie zawsze jest pojedynczy Output, nawet jeśli jest to typ jednostkowy ().
Stream definiuje również metodę do pobierania tych elementów. Nazywamy ją poll_next, aby jasno pokazać, że odpytuje w ten sam sposób, co Future::poll, i wytwarza sekwencję elementów w ten sam sposób, co Iterator::next. Jej typ zwracany łączy Poll z Option. Typ zewnętrzny to Poll, ponieważ musi być sprawdzany pod kątem gotowości, tak jak future. Typ wewnętrzny to Option, ponieważ musi sygnalizować, czy są więcej wiadomości, tak jak iterator.
Coś bardzo podobnego do tej definicji prawdopodobnie znajdzie się w standardowej bibliotece Rusta. W międzyczasie jest to część zestawu narzędzi większości środowisk wykonawczych, więc możesz na tym polegać, a wszystko, co omówimy dalej, powinno generalnie obowiązywać!
W przykładach, które widzieliśmy w sekcji „Strumienie: Futures w sekwencji”, nie użyliśmy jednak poll_next ani Stream, lecz next i StreamExt. Oczywiście mogliśmy pracować bezpośrednio w kategoriach API poll_next, ręcznie pisząc własne maszyny stanów Stream, tak samo jak mogliśmy pracować z futures bezpośrednio za pośrednictwem ich metody poll. Użycie await jest jednak znacznie przyjemniejsze, a cecha StreamExt dostarcza metodę next, dzięki czemu możemy to zrobić:
#![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;
// other methods...
}
}
Uwaga: Rzeczywista definicja, której użyliśmy wcześniej w rozdziale, wygląda nieco inaczej, ponieważ obsługuje wersje Rust, które nie obsługiwały jeszcze używania funkcji async w cechach. W rezultacie wygląda to tak:
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;Ten typ
Nexttostruct, która implementujeFuturei pozwala nam nazwać czas życia referencji doselfza pomocąNext<'_, Self>, tak abyawaitmógł działać z tą metodą.
Cecha StreamExt jest również miejscem, gdzie znajdują się wszystkie interesujące metody dostępne do użytku ze strumieniami. StreamExt jest automatycznie implementowana dla każdego typu, który implementuje Stream, ale te cechy są definiowane oddzielnie, aby umożliwić społeczności iterowanie po wygodnych API bez wpływu na podstawową cechę.
W wersji StreamExt użytej w skrzynce trpl cecha ta nie tylko definiuje metodę next, ale także dostarcza domyślną implementację next, która poprawnie obsługuje szczegóły wywoływania Stream::poll_next. Oznacza to, że nawet gdy musisz napisać własny typ danych strumieniowych, tylko musisz zaimplementować Stream, a następnie każdy, kto używa twojego typu danych, może automatycznie używać StreamExt i jego metod.
To wszystko, co omówimy w kwestii niskopoziomowych szczegółów tych cech. Na zakończenie zastanówmy się, jak futures (w tym strumienie), zadania i wątki pasują do siebie!