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

Les futures et la syntaxe async

Les éléments clés de la programmation asynchrone en Rust sont les futures et les mots-clés async et await de Rust.

Une future est une valeur qui n’est peut-être pas prête maintenant mais qui le deviendra à un moment donné dans le futur. (Ce même concept apparaît dans de nombreux langages, parfois sous d’autres noms comme task ou promise.) Rust fournit un trait Future comme brique de base pour que différentes opérations async puissent être implémentées avec différentes structures de données mais avec une interface commune. En Rust, les futures sont des types qui implémentent le trait Future. Chaque future contient ses propres informations sur la progression réalisée et sur ce que signifie « prêt ».

Vous pouvez appliquer le mot-clé async aux blocs et aux fonctions pour spécifier qu’ils peuvent être interrompus et repris. Au sein d’un bloc async ou d’une fonction async, vous pouvez utiliser le mot-clé await pour attendre une future (c’est-à-dire attendre qu’elle devienne prête). Tout point où vous attendez une future dans un bloc ou une fonction async est un endroit potentiel pour que ce bloc ou cette fonction se mette en pause et reprenne. Le processus de vérification auprès d’une future pour voir si sa valeur est disponible s’appelle le polling (ou interrogation).

Certains autres langages, comme C# et JavaScript, utilisent également les mots-clés async et await pour la programmation asynchrone. Si vous êtes familier avec ces langages, vous remarquerez peut-être des différences significatives dans la façon dont Rust gère la syntaxe. C’est pour de bonnes raisons, comme nous le verrons !

Lorsque nous écrivons du Rust async, nous utilisons les mots-clés async et await la plupart du temps. Rust les compilé en code équivalent utilisant le trait Future, de la même manière qu’il compilé les boucles for en code équivalent utilisant le trait Iterator. Comme Rust fournit le trait Future, vous pouvez aussi l’implémenter pour vos propres types de données quand vous en avez besoin. Beaucoup de fonctions que nous verrons tout au long de ce chapitre retournent des types avec leurs propres implémentations de Future. Nous reviendrons à la définition du trait à la fin du chapitre pour approfondir son fonctionnement, mais ces détails suffisent pour avancer.

Tout cela peut sembler un peu abstrait, alors écrivons notre premier programme async : un petit web scraper. Nous passerons deux URL depuis la ligne de commande, les récupérerons toutes les deux de manière concurrente, et retournerons le résultat de celle qui finit en premier. Cet exemple comportera pas mal de nouvelle syntaxe, mais ne vous inquiétez pas — nous expliquerons tout ce que vous devez savoir au fur et à mesure.

Notre premier programme async

Pour garder l’attention de ce chapitre sur l’apprentissage de l’async plutôt que sur la gestion de l’écosystème, nous avons créé le crate trpl (trpl est l’abréviation de « The Rust Programming Language »). Il ré-exporte tous les types, traits et fonctions dont vous aurez besoin, principalement depuis les crates [futures][futures-crate] et [tokio][tokio]. Le crate futures est le lieu officiel d’expérimentation de Rust pour le code async, et c’est en fait là que le trait Future a été conçu à l’origine. Tokio est le runtime async le plus largement utilisé en Rust aujourd’hui, surtout pour les applications web. Il existe d’autres excellents runtimes qui pourraient être plus adaptés à vos besoins. Nous utilisons le crate tokio en coulisses pour trpl car il est bien testé et largement utilisé.

Dans certains cas, trpl renomme ou encapsule les API d’origine pour vous permettre de vous concentrer sur les détails pertinents pour ce chapitre. Si vous voulez comprendre ce que fait le crate, nous vous encourageons à consulter [son code source][crate-source]. Vous pourrez voir de quel crate provient chaque ré-exportation, et nous avons laissé des commentaires détaillés expliquant ce que fait le crate.

Créez un nouveau projet binaire nommé hello-async et ajoutez le crate trpl comme dépendance :

$ cargo new hello-async
$ cd hello-async
$ cargo add trpl

Nous pouvons maintenant utiliser les différents éléments fournis par trpl pour écrire notre premier programme async. Nous allons construire un petit outil en ligne de commande qui récupère deux pages web, extrait l’élément <title> de chacune, et affiche le titre de la page qui terminé ce processus en premier.

Définition de la fonction page_title

Commençons par écrire une fonction qui prend une URL de page en paramètre, effectue une requête vers celle-ci, et retourné le texte de l’élément <title> (voir l’encart 17-1).

Filename: src/main.rs
extern crate trpl; // required for mdbook test

fn main() {
    // TODO: we'll add this next!
}

use trpl::Html;

async fn page_title(url: &str) -> Option<String> {
    let response = trpl::get(url).await;
    let response_text = response.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}
Listing 17-1: Defining an async function to get the title element from an HTML page

Premièrement, nous définissons une fonction nommée page_title et la marquons avec le mot-clé async. Ensuite, nous utilisons la fonction trpl::get pour récupérer l’URL passée en paramètre et ajoutons le mot-clé await pour attendre la réponse. Pour obtenir le texte de la réponse, nous appelons sa méthode text et l’attendons à nouveau avec le mot-clé await. Ces deux étapes sont asynchrones. Pour la fonction get, nous devons attendre que le serveur renvoie la première partie de sa réponse, qui inclura les en-têtes HTTP, les cookies, etc., et qui peut être délivrée séparément du corps de la réponse. Surtout si le corps est très volumineux, il peut falloir un certain temps pour qu’il arrive en entier. Comme nous devons attendre l’intégralité de la réponse, la méthode text est également async.

Nous devons explicitement attendre ces deux futures, car les futures en Rust sont paresseuses : elles ne font rien tant que vous ne leur demandez pas avec le mot-clé await. (En fait, Rust affichera un avertissement du compilateur si vous n’utilisez pas une future.) Cela pourrait vous rappeler la discussion sur les itérateurs dans la section [« Traitement d’une série d’éléments avec les itérateurs »][iterators-lazy] du chapitre 13. Les itérateurs ne font rien tant que vous n’appelez pas leur méthode next — que ce soit directement ou en utilisant des boucles for ou des méthodes comme map qui utilisent next en coulisses. De même, les futures ne font rien tant que vous ne leur demandez pas explicitement. Cette paresse permet à Rust d’éviter d’exécuter du code async tant que ce n’est pas réellement nécessaire.

section in Chapter 16, where the closure we passed to another thread started running immediately. It’s also différent from how many other languages approach async. But it’s important for Rust to be able to provide its performance guarantees, just as it is with iterators. –> Remarque : cela est différent du comportement que nous avons vu en utilisant thread::spawn dans la section [« Créer un nouveau thread avec spawn »][thread-spawn] du chapitre 16, où la closure que nous avons passée à un autre thread commençait à s’exécuter immédiatement. C’est aussi différent de la façon dont beaucoup d’autres langages abordent l’async. Mais il est important que Rust puisse fournir ses garanties de performance, tout comme avec les itérateurs.

Une fois que nous avons response_text, nous pouvons l’analyser en une instance du type Html en utilisant Html::parse. Au lieu d’une chaîne brute, nous avons maintenant un type de données que nous pouvons utiliser pour travailler avec le HTML comme une structure de données plus riche. En particulier, nous pouvons utiliser la méthode select_first pour trouver la première instance d’un sélecteur CSS donné. En passant la chaîne "title", nous obtiendrons le premier élément <title> du document, s’il y en à un. Comme il peut ne pas y avoir d’élément correspondant, select_first retourné un Option<ElementRef>. Enfin, nous utilisons la méthode Option::map, qui nous permet de travailler avec l’élément dans l’Option s’il est présent, et de ne rien faire sinon. (Nous pourrions aussi utiliser une expression match ici, mais map est plus idiomatique.) Dans le corps de la fonction que nous fournissons à map, nous appelons inner_html sur le title pour obtenir son contenu, qui est une String. Au final, nous avons un Option<String>.

Remarquez que le mot-clé await de Rust vient après l’expression que vous attendez, et non avant. C’est-à-dire que c’est un mot-clé postfixe. Cela peut différer de ce à quoi vous êtes habitué si vous avez utilisé async dans d’autres langages, mais en Rust, cela rend les chaînes de méthodes beaucoup plus agréables à utiliser. En conséquence, nous pourrions modifier le corps de page_title pour chaîner les appels de fonctions trpl::get et text avec await entre eux, comme montré dans l’encart 17-2.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    // TODO: we'll add this next!
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}
Listing 17-2: Chaining with the await keyword

Avec cela, nous avons écrit avec succès notre première fonction async ! Avant d’ajouter du code dans main pour l’appeler, parlons un peu plus de ce que nous avons écrit et de ce que cela signifie.

Quand Rust voit un bloc marqué avec le mot-clé async, il le compilé en un type de données unique et anonyme qui implémente le trait Future. Quand Rust voit une fonction marquée avec async, il la compilé en une fonction non-async dont le corps est un bloc async. Le type de retour d’une fonction async est le type du type de données anonyme que le compilateur crée pour ce bloc async.

Ainsi, écrire async fn est équivalent à écrire une fonction qui retourné une future du type de retour. Pour le compilateur, une définition de fonction telle que async fn page_title dans l’encart 17-1 est à peu près équivalente à une fonction non-async définie comme ceci : rust extern crate trpl; // required for mdbook test use std::future::Future; use trpl::Html; fn page_title(url: &str) -> impl Future<Output = Option<String>> { async move { let text = trpl::get(url).await.text().await; Html::parse(&text) .select_first("title") .map(|title| title.inner_html()) } }

#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
use std::future::Future;
use trpl::Html;

fn page_title(url: &str) -> impl Future<Output = Option<String>> {
    async move {
        let text = trpl::get(url).await.text().await;
        Html::parse(&text)
            .select_first("title")
            .map(|title| title.inner_html())
    }
}
}

Parcourons chaque partie de la version transformée :

  • Elle utilise la syntaxe impl Trait que nous avons abordée au chapitre 10 dans la section [« Les traits comme paramètres »][impl-trait].
  • La valeur retournée implémente le trait Future avec un type associé Output. Remarquez que le type Output est Option<String>, qui est le même que le type de retour original de la version async fn de page_title.
  • Tout le code appelé dans le corps de la fonction originale est encapsulé dans un bloc async move. Rappelez-vous que les blocs sont des expressions. Ce bloc entier est l’expression retournée par la fonction.
  • Ce bloc async produit une valeur de type Option<String>, comme décrit précédemment. Cette valeur correspond au type Output dans le type de retour. C’est comme les autres blocs que vous avez vus.
  • Le nouveau corps de la fonction est un bloc async move en raison de la façon dont il utilise le paramètre url. (Nous parlerons beaucoup plus de async versus async move plus loin dans le chapitre.)

Nous pouvons maintenant appeler page_title dans main.

Exécuter une fonction async avec un runtime

Pour commencer, nous allons obtenir le titre d’une seule page, comme montré dans l’encart 17-3. Malheureusement, ce code ne compilé pas encore.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use trpl::Html;

async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let url = &args[1];
    match page_title(url).await {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}
Listing 17-3: Calling the page_title function from main with a user-supplied argument

Nous suivons le même schéma que celui utilisé pour obtenir les arguments de la ligne de commande dans la section [« Accepter les arguments de la ligne de commande »][cli-args] du chapitre 12. Ensuite, nous passons l’argument URL à page_title et attendons le résultat. Comme la valeur produite par la future est un Option<String>, nous utilisons une expression match pour afficher différents messages selon que la page avait ou non un <title>.

Le seul endroit où nous pouvons utiliser le mot-clé await est dans les fonctions ou blocs async, et Rust ne nous laisse pas marquer la fonction spéciale main comme 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`

La raison pour laquelle main ne peut pas être marquée async est que le code async a besoin d’un runtime : un crate Rust qui gère les détails de l’exécution du code asynchrone. La fonction main d’un programme peut initialiser un runtime, mais elle n’est pas un runtime elle-même. (Nous verrons bientôt pourquoi c’est le cas.) Tout programme Rust qui exécute du code async a au moins un endroit où il configure un runtime qui exécute les futures.

La plupart des langages qui supportent l’async intègrent un runtime, mais pas Rust. À la place, il existe de nombreux runtimes async différents disponibles, chacun faisant des compromis différents adaptés au cas d’utilisation qu’il cible. Par exemple, un serveur web à haut débit avec de nombreux cœurs CPU et une grande quantité de RAM à des besoins très différents d’un microcontrôleur avec un seul cœur, une petite quantité de RAM et aucune capacité d’allocation sur le tas. Les crates qui fournissent ces runtimes offrent souvent aussi des versions async de fonctionnalités courantes comme les E/S de fichiers ou réseau.

Ici, et tout au long du reste de ce chapitre, nous utiliserons la fonction block_on du crate trpl, qui prend une future en argument et bloque le thread courant jusqu’à ce que cette future s’exécute jusqu’à la fin. En coulisses, appeler block_on configure un runtime en utilisant le crate tokio qui est utilisé pour exécuter la future passée en paramètre (le comportement de block_on du crate trpl est similaire aux fonctions block_on d’autres crates de runtime). Une fois que la future est terminée, block_on retourné la valeur que la future a produite.

Nous pourrions passer la future retournée par page_title directement à block_on et, une fois terminée, faire un match sur le Option<String> résultant comme nous avons essayé de le faire dans l’encart 17-3. Cependant, pour la plupart des exemples du chapitre (et la plupart du code async dans le monde réel), nous ferons plus qu’un simple appel de fonction async, donc à la place nous passerons un bloc async et attendrons explicitement le résultat de l’appel à page_title, comme dans l’encart 17-4.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let url = &args[1];
        match page_title(url).await {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
    })
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}
Listing 17-4: Awaiting an async block with trpl::block_on

Quand nous exécutons ce code, nous obtenons le comportement attendu initialement :

$ 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'`
The title for https://www.rust-lang.org was
            Rust Programming Language

Ouf — nous avons enfin du code async qui fonctionne ! Mais avant d’ajouter le code pour mettre en compétition deux sites l’un contre l’autre, tournons brièvement notre attention vers le fonctionnement des futures.

Chaque point d’attente — c’est-à-dire chaque endroit où le code utilise le mot-clé await — représente un endroit où le contrôle est rendu au runtime. Pour que cela fonctionne, Rust doit garder une trace de l’état impliqué dans le bloc async afin que le runtime puisse lancer un autre travail puis revenir quand il est prêt à essayer de faire avancer le premier à nouveau. C’est une machine à états invisible, comme si vous aviez écrit un enum comme celui-ci pour sauvegarder l’état courant à chaque point d’attente : rust {{#rustdoc_include ../listings/ch17-async-await/no-listing-state-machine/src/lib.rs:enum}}

#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test

enum PageTitleFuture<'a> {
    Initial { url: &'a str },
    GetAwaitPoint { url: &'a str },
    TextAwaitPoint { response: trpl::Response },
}
}

Écrire le code pour effectuer la transition entre chaque état à la main serait fastidieux et source d’erreurs, surtout quand vous devez ajouter plus de fonctionnalités et plus d’états au code par la suite. Heureusement, le compilateur Rust crée et gère automatiquement les structures de données de la machine à états pour le code async. Les règles normales d’emprunt et de possession autour des structures de données s’appliquent toujours, et heureusement, le compilateur gère aussi la vérification de celles-ci pour nous et fournit des messages d’erreur utiles. Nous en verrons quelques-uns plus loin dans le chapitre.

Au final, quelque chose doit exécuter cette machine à états, et ce quelque chose est un runtime. (C’est pourquoi vous pouvez rencontrer des mentions d’exécuteurs quand vous vous renseignez sur les runtimes : un exécuteur est la partie d’un runtime responsable de l’exécution du code async.)

Vous pouvez maintenant voir pourquoi le compilateur nous a empêchés de faire de main elle-même une fonction async dans l’encart 17-3. Si main était une fonction async, quelque chose d’autre devrait gérer la machine à états pour la future que main retournerait, mais main est le point d’entrée du programme ! À la place, nous avons appelé la fonction trpl::block_on dans main pour configurer un runtime et exécuter la future retournée par le bloc async jusqu’à ce qu’elle soit terminée.

Remarque : certains runtimes fournissent des macros pour que vous puissiez écrire une fonction main async. Ces macros réécrivent async fn main() { ... } en un fn main normal, qui fait la même chose que ce que nous avons fait manuellement dans l’encart 17-4 : appeler une fonction qui exécute une future jusqu’à son achèvement de la même manière que trpl::block_on.

Maintenant, assemblons ces éléments et voyons comment nous pouvons écrire du code concurrent.

Mettre en compétition deux URL de manière concurrente

Dans l’encart 17-5, nous appelons page_title avec deux URL différentes passées depuis la ligne de commande et les mettons en compétition en sélectionnant la future qui terminé en premier.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use trpl::{Either, Html};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let title_fut_1 = page_title(&args[1]);
        let title_fut_2 = page_title(&args[2]);

        let (url, maybe_title) =
            match trpl::select(title_fut_1, title_fut_2).await {
                Either::Left(left) => left,
                Either::Right(right) => right,
            };

        println!("{url} returned first");
        match maybe_title {
            Some(title) => println!("Its page title was: '{title}'"),
            None => println!("It had no title."),
        }
    })
}

async fn page_title(url: &str) -> (&str, Option<String>) {
    let response_text = trpl::get(url).await.text().await;
    let title = Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html());
    (url, title)
}
Listing 17-5: Calling page_title for two URLs to see which returns first

Nous commençons par appeler page_title pour chacune des URL fournies par l’utilisateur. Nous sauvegardons les futures résultantes sous les noms title_fut_1 et title_fut_2. Rappelez-vous, elles ne font encore rien, car les futures sont paresseuses et nous ne les avons pas encore attendues. Ensuite, nous passons les futures à trpl::select, qui retourné une valeur indiquant laquelle des futures passées en paramètre se terminé en premier.

Remarque : en coulisses, trpl::select est construit sur une fonction select plus générale définie dans le crate futures. La fonction select du crate futures peut faire beaucoup de choses que la fonction trpl::select ne peut pas, mais elle a aussi une complexité supplémentaire que nous pouvons ignorer pour l’instant.

L’une où l’autre des futures peut légitimement « gagner », donc il n’est pas logique de retourner un Result. À la place, trpl::select retourné un type que nous n’avons pas vu auparavant, trpl::Either. Le type Either est quelque peu similaire à un Result en ce qu’il a deux cas. Contrairement à Result, cependant, il n’y à aucune notion de succès ou d’échec intégrée dans Either. À la place, il utilise Left et Right pour indiquer « l’un où l’autre » : rust enum Either<A, B> { Left(A), Right(B), }

#![allow(unused)]
fn main() {
enum Either<A, B> {
    Left(A),
    Right(B),
}
}

La fonction select retourné Left avec la sortie de cette future si le premier argument gagne, et Right avec la sortie du deuxième argument future si celui-là gagne. Cela correspond à l’ordre dans lequel les arguments apparaissent lors de l’appel de la fonction : le premier argument est à gauche du deuxième argument.

Nous mettons aussi à jour page_title pour retourner la même URL passée en paramètre. De cette façon, si la page qui retourné en premier n’a pas de <title> que nous pouvons résoudre, nous pouvons quand même afficher un message significatif. Avec ces informations disponibles, nous terminons en mettant à jour notre sortie println! pour indiquer à la fois quelle URL a terminé en premier et quel est, le cas échéant, le <title> de la page web à cette URL.

Vous avez maintenant construit un petit web scraper fonctionnel ! Choisissez quelques URL et exécutez l’outil en ligne de commande. Vous découvrirez peut-être que certains sites sont systématiquement plus rapides que d’autres, tandis que dans d’autres cas, le site le plus rapide varie d’une exécution à l’autre. Plus important encore, vous avez appris les bases du travail avec les futures, et nous pouvons maintenant approfondir ce que nous pouvons faire avec l’async.