Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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).

File: src/main.rs
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())
}
Listato 17-1: Definizione di una funzione asincrona per ottenere l’elemento del titolo da una pagina 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 con spawn, 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.

File: src/main.rs
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())
}
Listato 17-2: Concatenazione con la parola chiave 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 di Output. Nota che il type Output è Option<String>, che è lo stesso type di ritorno della versione async fn di titolo_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 type Output nel type di ritorno. È proprio come altri blocchi che hai visto.
  • Il nuovo corpo della funzione è un blocco async move per come usa il parametro url. (Confronteremo molto più approfonditamente async e async 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>.

File: src/main.rs
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())
}
Listato 17-3: Chiamare la funzione titolo_pagina da main con un argomento fornito dall’utente

Purtroppo, 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.

File: src/main.rs
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())
}
Listato 17-4: Eseguire ed attendere un blocco async con 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 riscrivono async fn main() { ... } per essere un normale fn 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 fa trpl::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.

File: src/main.rs
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)
}
Listato 17-5: Creazione di due future con chiamata a titolo_pagina per farle competere tra loro

Iniziamo 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 funzione select può fare molte cose che la funzione trpl::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.


  1. Macchina a Stati Finiti su wikipedia