Future e la Sintassi Async
Gli elementi chiave della programmazione asincrona in Rust sono le future e le
parole chiave async
e await
di Rust.
Una future è un valore che potrebbe non essere pronto ora, ma lo diventerà in
qualche momento in futuro. (Questo stesso concetto compare in molti linguaggi, a
volte sotto altri nomi come task o promise.) Rust fornisce un trait
Future
come blocco costruttivo in modo che diverse operazioni async possano
essere implementate con strutture dati diverse ma con un’interfaccia comune. In
Rust, le future sono type che implementano il trait Future
. Ogni
future contiene le proprie informazioni sui progressi fatti e su cosa
significa essere “pronti”.
Puoi applicare la parola chiave async
a blocchi e funzioni per specificare che
possono essere interrotti e ripresi. All’interno di un blocco async o di una
funzione async, puoi usare la parola chiave await
per attendere una future
(cioè, aspettare che sia pronta). Ogni punto in cui attendi una future
all’interno di un blocco o funzione async è un potenziale punto in cui quel
blocco o funzione async può mettersi in pausa e riprendere. Il processo di
verifica con una future per vedere se il suo valore è già disponibile è
chiamato polling.
Alcuni altri linguaggi, come C# e JavaScript, usano parole chiave async
e
await
per la programmazione asincrona. Se hai familiarità con questi
linguaggi, potresti notare alcune differenze significative nel modo in cui Rust
fa le cose, incluso come gestisce la sintassi. E questo è per una buona ragione,
come vedremo!
Quando scriviamo codice async in Rust, usiamo la maggior parte delle volte le
parole chiave async
e await
. Rust le compila in codice equivalente usando il
trait Future
, proprio come compila i cicli for
in codice equivalente
usando il trait Iterator
. Poiché Rust fornisce il trait Future
, puoi
anche implementarlo per i type da te definiti quando ne hai bisogno. Molte
delle funzioni che vedremo in questo capitolo restituiscono type con le
proprie implementazioni di Future
. Torneremo alla definizione del trait alla
fine del capitolo e approfondiremo come funziona, ma questi dettagli sono
sufficienti per procedere.
Tutto questo potrebbe sembrare un po’ astratto, quindi scriviamo il nostro primo programma async: un piccolo web scraper (estrattore info da pagine web). Passeremo due URL dalla riga di comando, li recupereremo contemporaneamente e restituiremo il risultato di quello che finisce per primo. Questo esempio avrà parecchia nuova sintassi, ma non preoccuparti, spiegheremo tutto ciò che serve sapere man mano che procediamo.
Il Nostro Primo Programma Async
Per mantenere l’attenzione di questo capitolo sull’apprendimento di async
piuttosto che sulla gestione di parti dell’ecosistema, abbiamo creato il crate
trpl
(trpl
è abbreviazione di “The Rust Programming
Language”). Riesporta tutti i type, i trait e le funzioni di cui avrai
bisogno, principalmente dai crate futures
e
tokio
. Il crate futures
è la sede ufficiale per la
sperimentazione Rust del codice async, ed è in realtà dove il trait Future
è stato originariamente progettato. Tokio è il runtime async più
utilizzato in Rust oggi, specialmente per applicazioni web. Ci sono altri ottimi
runtime là fuori, e potrebbero essere più adatti ai tuoi scopi. Usiamo il
crate tokio
come base per trpl
perché è ben testato e ampiamente
utilizzato.
In alcuni casi, trpl
rinomina o incapsula le API originali per mantenerti
concentrato sui dettagli rilevanti per questo capitolo. Se vuoi capire cosa fa
il crate, ti incoraggiamo a controllare il suo codice
sorgente. Sarai in grado di vedere da quale
crate proviene ogni riesportazione, e abbiamo lasciato commenti esaurienti che
spiegano cosa fa il crate.
Crea un nuovo progetto binario chiamato hello-async
e aggiungi il crate
trpl
come dipendenza:
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
Ora possiamo usare i vari pezzi forniti da trpl
per scrivere il nostro primo
programma async. Costruiremo un piccolo strumento da riga di comando che
recupera due pagine web, estrae l’elemento <title>
da ciascuna e stampa il
titolo della pagina che completa per prima l’intero processo.
Definire la Funzione titolo_pagina
Iniziamo scrivendo una funzione che prende un URL di una pagina come parametro, la scarica e restituisce il testo dell’elemento del titolo (vedi Listato 17-1).
extern crate trpl; // necessario per test mdbook fn main() { // TODO: lo aggiungeremo in seguito! } use trpl::Html; async fn titolo_pagina(url: &str) -> Option<String> { let risposta = trpl::get(url).await; let testo_risposta = risposta.text().await; Html::parse(&testo_risposta) .select_first("title") .map(|titolo| titolo.inner_html()) }
Per prima cosa, definiamo una funzione chiamata titolo_pagina
e la
contrassegniamo con la parola chiave async
. Poi usiamo la funzione trpl::get
per recuperare l’URL passato e aggiungiamo la parola chiave await
per
aspettare la risposta. Per ottenere il testo della risposta, chiamiamo il suo
metodo text
e di nuovo aspettiamo con la parola chiave await
. Entrambi
questi passaggi sono asincroni.
Per la funzione get
, dobbiamo aspettare che il server invii la prima parte
della sua risposta, che includerà intestazioni HTTP, cookie e così via, e può
essere consegnata separatamente dal corpo della risposta. Soprattutto se il
corpo è molto grande, può volerci del tempo perché arrivi tutto. Poiché dobbiamo
aspettare l’intera risposta, anche il metodo text
è asincrono.
Dobbiamo esplicitamente attendere entrambi queste future, perché le future
in Rust sono lazy (pigre): non fanno nulla finché non le chiedi di farlo con
la parola chiave await
. (In effetti, Rust mostrerà un avviso del compilatore
se non usi una future.) Questo potrebbe ricordarti la discussione del Capitolo
13 sugli iteratori nella sezione Elaborare una Serie di Elementi con
Iteratori. Gli iteratori non fanno nulla a meno
che non chiami il loro metodo next
, sia direttamente che usando cicli for
o
metodi come map
che usano next
sotto il cofano. Allo stesso modo, le
future non fanno nulla a meno che tu non le chieda esplicitamente di farlo.
Questa pigrizia permette a Rust di evitare di eseguire codice asincrono finché
non è effettivamente necessario.
Nota: Questo è diverso dal comportamento che abbiamo visto nel capitolo precedente quando abbiamo usato
thread::spawn
in Creare un Nuovo Thread conspawn
, dove la chiusura passata a un altro thread veniva eseguita immediatamente. È anche diverso da come molti altri linguaggi gestiscono l’asincronia. Ma è importante per Rust poter fornire le sue garanzie di prestazioni, proprio come accade con gli iteratori.
Una volta che abbiamo testo_risposta
, possiamo analizzarlo in un’istanza del
type Html
usando Html::parse
. Invece di una stringa grezza, ora abbiamo un
tipo di dato che possiamo usare per lavorare con l’HTML come una struttura dati
più funzionale. In particolare, possiamo usare il metodo select_first
per
trovare la prima istanza di un dato selettore CSS. Passando la stringa
"title"
, otterremo il primo elemento <title>
nel documento, se presente.
Poiché potrebbe non esserci alcun elemento corrispondente, select_first
restituisce un Option<ElementRef>
. Infine, usiamo il metodo Option::map
, che
ci permette di lavorare sull’elemento nell’Option
se è presente, e non fare
nulla se non lo è. (Potremmo anche usare un’espressione match
, ma map
è più
idiomatico.) Nel corpo della funzione che forniamo a map
, chiamiamo
inner_html
su titolo
per ottenere il suo contenuto, che è una String
. Alla
fine dei conti, abbiamo un Option<String>
.
Nota che la parola chiave await
di Rust va dopo l’espressione che stai
attendendo, non prima. Cioè, è una parola chiave post-fissa. Questo potrebbe
differire da ciò a cui sei abituato se hai usato async
in altri linguaggi, ma
in Rust rende le catene di metodi molto più gradevoli da gestire. Di
conseguenza, possiamo modificare il corpo di titolo_pagina
per concatenare le
chiamate di funzione trpl::get
e text
con await
in mezzo, come mostrato
nel Listato 17-2.
extern crate trpl; // necessario per test mdbook use trpl::Html; fn main() { // TODO: lo aggiungeremo in seguito! } async fn titolo_pagina(url: &str) -> Option<String> { let testo_risposta = trpl::get(url).await.text().await; Html::parse(&testo_risposta) .select_first("title") .map(|titolo| titolo.inner_html()) }
await
Con questo, abbiamo scritto con successo la nostra prima funzione asincrona!
Prima di aggiungere del codice in main
per chiamarla, parliamo un po’ di più
di cosa abbiamo scritto e cosa significa.
Quando Rust vede un blocco contrassegnato con la parola chiave async
, lo
compila in un type anonimo e univoco che implementa il trait Future
.
Quando Rust vede una funzione contrassegnata con async
, la compila in una
funzione non asincrona il cui corpo è un blocco asincrono. Il type di ritorno
di una funzione asincrona è il type anonimo che il compilatore crea per quel
blocco asincrono.
Quindi, scrivere async fn
è equivalente a scrivere una funzione che
restituisce una future del type di ritorno. Per il compilatore, una
definizione di funzione come async fn titolo_pagina
nel Listato 17-1 è
equivalente a una funzione non asincrona definita in questo modo:
#![allow(unused)] fn main() { extern crate trpl; // necessario per test mdbook use std::future::Future; use trpl::Html; fn titolo_pagina(url: &str) -> impl Future<Output = Option<String>> { async move { let testo_risposta = trpl::get(url).await.testo_risposta().await; Html::parse(&testo_risposta) .select_first("title") .map(|titolo| titolo.inner_html()) } } }
Analizziamo ogni parte della versione trasformata:
- Usa la sintassi
impl Trait
che abbiamo discusso nel Capitolo 10 nella sezione “Usare i Trait come Parametri”. - Il trait restituito è una
Future
con un type associato diOutput
. Nota che il typeOutput
èOption<String>
, che è lo stesso type di ritorno della versioneasync fn
dititolo_pagina
. - Tutto il codice chiamato nel corpo della funzione originale è racchiuso in un
blocco
async move
. Ricorda che i blocchi sono espressioni. Questo intero blocco è l’espressione restituita dalla funzione. - Questo blocco asincrono produce un valore di type
Option<String>
, come appena descritto. Quel valore corrisponde al typeOutput
nel type di ritorno. È proprio come altri blocchi che hai visto. - Il nuovo corpo della funzione è un blocco
async move
per come usa il parametrourl
. (Confronteremo molto più approfonditamenteasync
easync move
più avanti in questo capitolo).
Ora possiamo chiamare titolo_pagina
in main
.
Determinare il Titolo di una Singola Pagina
Per iniziare, prenderemo il titolo di una singola pagina. Nel Listato 17-3,
seguiamo lo stesso schema che abbiamo usato nel Capitolo 12 per Ricevere
Argomenti dalla Riga di Comando. Poi passiamo il primo
URL a titolo_pagina
e attendiamo il risultato. Poiché il valore prodotto dalla
future è un Option<String>
, usiamo un’espressione match
per stampare
messaggi diversi a seconda che la pagina abbia o meno un <title>
.
extern crate trpl; // necessario per test mdbook
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match titolo_pagina(url).await {
Some(titolo) => println!("Il titolo per {url} era {titolo}"),
None => println!("{url} non aveva titolo"),
}
}
async fn titolo_pagina(url: &str) -> Option<String> {
let testo_risposta = trpl::get(url).await.text().await;
Html::parse(&testo_risposta)
.select_first("title")
.map(|titolo| titolo.inner_html())
}
titolo_pagina
da main
con un argomento fornito dall’utentePurtroppo, questo codice non si compila. L’unico posto in cui possiamo usare la
parola chiave await
è in funzioni o blocchi async, e Rust non ci permette di
contrassegnare la funzione main
speciale come async
.
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
Il motivo per cui main
non può essere contrassegnata async
è che il codice
async ha bisogno di un runtime: un crate Rust che gestisce i dettagli
dell’esecuzione del codice asincrono. La funzione main
di un programma può
inizializzare un runtime, ma non è un runtime in sé. (Vedremo più avanti
perché è così.) Ogni programma Rust che esegue codice asincrono ha almeno un
punto in cui configura un runtime ed esegue le future.
La maggior parte dei linguaggi che supportano async includono un runtime, ma Rust no. Invece, ci sono molti runtime asincroni disponibili, ognuno dei quali fa compromessi diversi adatti al caso d’uso che intende coprire. Ad esempio, un server web che gestisce grandi quantità di dati eseguito su CPU multi-core e una grande quantità di RAM ha esigenze molto diverse da un micro-controllore con un singolo core, poca RAM e nessuna capacità di allocazione nell’heap. I crate che forniscono questi runtime spesso forniscono anche versioni async di funzionalità comuni come I/O su file o di rete.
Qui, e nel resto di questo capitolo, useremo la funzione run
del crate
trpl
, che prende una future come argomento e la esegue fino al
completamento. Dietro le quinte, chiamare run
configura un runtime usato per
eseguire la future passata. Una volta che la future è completata, run
restituisce qualsiasi valore che la future ha prodotto.
Potremmo passare direttamente la future restituita da titolo_pagina
a run
,
e una volta completata, potremmo fare il match sul risultante
Option<String>
, come abbiamo provato a fare nel Listato 17-3. Tuttavia, per la
maggior parte degli esempi in questo capitolo (e per la maggior parte del codice
async nel mondo reale), faremo più di una singola chiamata di funzione
async, quindi invece passeremo un blocco async
ed esplicitamente attendiamo
il risultato della chiamata titolo_pagina
, come nel Listato 17-4.
extern crate trpl; // necessario per test mdbook
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let url = &args[1];
match titolo_pagina(url).await {
Some(titolo) => println!("Il titolo per {url} era {titolo}"),
None => println!("{url} non aveva titolo"),
}
})
}
async fn titolo_pagina(url: &str) -> Option<String> {
let testo_risposta = trpl::get(url).await.text().await;
Html::parse(&testo_risposta)
.select_first("title")
.map(|titolo| titolo.inner_html())
}
trpl::run
Quando eseguiamo questo codice, otteniamo il comportamento che avevamo inizialmente previsto:
$ cargo run -- https://www.rust-lang.org
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/async_await 'https://www.rust-lang.org'`
Il titolo per https://www.rust-lang.org era
Rust Programming Language
Bene! Finalmente abbiamo del codice async funzionante! Ma prima di aggiungere il codice per mettere a gara i due siti l’uno contro l’altro, dedichiamo brevemente la nostra attenzione a come funzionano le future.
Ogni punto di attesa (await point) - cioè, ogni punto in cui il codice usa
la parola chiave await
- rappresenta un punto in cui il controllo viene
restituito al runtime. Perché la cosa funzioni, Rust deve tenere traccia dello
stato nel blocco async in modo che il runtime possa avviare altro lavoro e
poi tornare quando è pronto per provare a far avanzare il primo. Questa è in
pratica una macchina a stati finiti1, come se avessi scritto un enum
come questo per salvare lo stato corrente ad ogni punto di attesa:
#![allow(unused)] fn main() { extern crate trpl; // necessario per test mdbook enum TitoloPaginaFuture<'a> { Iniziale { url: &'a str }, PrendiPuntoAttesa { url: &'a str }, TestoPuntoAttesa { risposta: trpl::Response }, } }
Scrivere il codice per passare manualmente tra ogni stato sarebbe laborioso e soggetto a errori, soprattutto quando è necessario aggiungere più funzionalità e più stati al codice in seguito. Fortunatamente, il compilatore Rust crea e gestisce automaticamente le strutture dati della macchina a stati per il codice async. Tutte le normali regole di prestito e ownership intorno alle strutture dati si applicano ancora, e felicemente, il compilatore gestisce anche la verifica di quelle per noi e fornisce messaggi di errore utili. Ne esamineremo alcuni più avanti in questo capitolo.
Alla fine, qualcosa deve eseguire questa macchina a stati, e quella cosa è un runtime. (Questo è il motivo per cui potresti imbatterti in riferimenti a executor quando cerchi informazioni sui runtime: un executor è la parte di un runtime responsabile dell’esecuzione del codice async.)
Ora puoi vedere perché il compilatore ci ha impedito di rendere main
stesso
una funzione async nel Listato 17-3. Se main
fosse una funzione async,
qualcos’altro dovrebbe gestire la macchina a stati per qualsiasi future che
main
restituisse, ma main
è il punto di partenza del programma! Invece,
abbiamo chiamato la funzione trpl::run
in main
per configurare un runtime
ed eseguire la future restituita dal blocco async
fino al suo completamento.
Nota: Alcuni runtime forniscono macro in modo che tu possa scrivere una funzione
main
async. Quelle macro riscrivonoasync fn main() { ... }
per essere un normalefn main
, che fa la stessa cosa che abbiamo fatto a mano nel Listato 17-4: chiamare una funzione che esegue una future fino al completamento proprio come fatrpl::run
.
Ora mettiamo insieme questi pezzi e vediamo come possiamo scrivere codice concorrente.
Mettere a Gara i Due URL l’Uno Contro l’Altro
Nel Listato 17-5, chiamiamo titolo_pagina
con due URL diversi passati dalla
riga di comando e li mettiamo a gara.
extern crate trpl; // necessario per test mdbook
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let titolo_fut_1 = titolo_pagina(&args[1]);
let titolo_fut_2 = titolo_pagina(&args[2]);
let (url, forse_titolo) =
match trpl::race(titolo_fut_1, titolo_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} ritornato per primo");
match forse_titolo {
Some(titolo) => println!("Il suo titolo era: '{titolo}'"),
None => println!("Non aveva titolo."),
}
})
}
async fn titolo_pagina(url: &str) -> (&str, Option<String>) {
let testo_risposta = trpl::get(url).await.text().await;
let titolo = Html::parse(&testo_risposta)
.select_first("title")
.map(|titolo| titolo.inner_html());
(url, titolo)
}
titolo_pagina
per farle competere tra loroIniziamo chiamando titolo_pagina
per ciascuno degli URL forniti dall’utente.
Salviamo le future risultanti come titolo_fut_1
e titolo_fut_2
. Ricorda,
queste non fanno ancora nulla, perché le future sono lazy e non le abbiamo
ancora messe in coda. Poi passiamo le future a trpl::race
, che restituisce
un valore per indicare quale delle future a esso passate finisce per prima.
Nota: Sotto il cofano,
race
è costruito su una funzione più generale,select
, che incontrerai più spesso nel codice Rust reale. Una funzioneselect
può fare molte cose che la funzionetrpl::race
non può, ma ha anche alcune complessità aggiuntive che possiamo tralasciare per ora.
Può legittimamente “vincere” una qualsiasi delle future, quindi non ha senso
restituire un Result
. Invece, race
restituisce un type che non abbiamo
ancora visto, trpl::Either
. Il type Either
è in qualche modo simile a un
Result
in quanto ha due casi. A differenza di Result
, però, non c’è alcuna
nozione di successo o fallimento incorporata in Either
. Invece, usa Left
e
Right
per indicare “l’uno o l’altro”:
#![allow(unused)] fn main() { enum Either<A, B> { Left(A), Right(B), } }
La funzione race
restituisce Left
con l’output dalla prima future che
finisce, o Right
con l’output della seconda future se quella finisce per
prima. Questo corrisponde all’ordine in cui appaiono gli argomenti quando si
chiama la funzione: il primo argomento è a sinistra del secondo argomento.
Aggiorniamo anche titolo_pagina
per restituire lo stesso URL passato. In modo
che, se la pagina che restituisce per prima non ha un <title>
che possiamo
risolvere, possiamo comunque stampare un messaggio significativo. Con queste
informazioni disponibili, concludiamo aggiornando l’output di println!
per
indicare sia quale URL ha finito per primo, sia qual è il <title>
, se
presente, per la pagina web a quell’URL.
Hai costruito ora un piccolo web scraper funzionante! Scegli un paio di URL ed esegui lo strumento da riga di comando. Potresti scoprire che alcuni siti sono costantemente più veloci di altri, mentre in altri casi il sito più veloce varia da un’esecuzione all’altra. Cosa più importante, hai imparato le basi del lavoro con le future, quindi ora possiamo approfondire cosa possiamo fare con async.