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 de Async

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

Une future est une valeur qui peut ne pas être prête maintenant, mais qui deviendra prête dans le futur (ce même concept apparaît dans beaucoup de langages, parfois avec d’autres noms comme tâche ou promesse). Rust fournit un trait Future comme brique élémentaire de manière à ce que les différentes opérations asynchrones peuvent être implémentées avec différentes structures de données mais avec une interface commune. En Rust, les futures sont les types qui implémentent le trait Future. Chaque future renferme son information propre concernant les progrès accomplis et la signification de “prêt”.

Vous pouvez appliquer le mot-clé async à des blocs et fonctions pour spécifier qu’ils peuvent être interrompus et restaurés. À l’intérieur d’un bloc async ou d’une fonction async, vous pouvez utiliser le mot-clé await pour attendre une future (ce qui veut dire attendre qu’elle devienne prête). Chaque endroit où vous attendez une future avec un bloc async ou une fonction async est un endroit potentiel de pause et de reprise d’activité. Le processus de vérification auprès d’une future pour voir si sa valeur est disponible s’appelle le polling.

D’autres langages, comme le C# ou le JavaScript, utilisent également les mot-clésasync et await pour la programmation asynchrone. Si vous êtes déjà familier avec ces langages, vous notez peut-être quelques différences notables dans la manière dont Rust gère cette syntaxe. Il y a de bonnes raisons à cela, comme nous allons le voir !

Quand on écrit du Rust asynchrone, nous utilisons les mot-clés async et await la plupart du temps. Rust les compile en code équivalent à l’aide du trait Future, de manière similaire à quand il compile les boucles for dans un code équivalent en utilisant le trait Iterator. Toutefois, 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 des fonctions que nous allons voir au cours de ce chapitre renvoient des types ayant leur propres implémentations de Future. Nous reviendrons à la définition du trait à la fin de ce chapitre et approfondirons plus sur la manière dont il fonctionne, mais nous avons là assez de détails pour aller plus avant.

Tout ceci peut sembler un peu théorique, donc écrivons notre premier programme asynchrone : un petit outil de collecte de données Web. Nous lui fournirons deux URLs via la ligne de commande, nous récupèrerons les deux de manière concurrente, et renverrons l’URL qui se termine en premier. Cet exemple comportera une bonne part de syntaxe nouvelle, mais ne vous en faites pas — nous expliquerons tout ce que vous devez savoir au fur et à mesure.

Notre premier programme asynchrone

Afin que ce chapitre reste concentré sur l’apprentissage d’async au lieu de jongler avec des parties de l’écosystème, nous avons créé la crate trpl (trpl est l’abréviation de “The Rust Programming Language”). Elle re-exporte tous les types, traits et fonctions dont vous aurez besoin, principalement depuis les crates futures et tokio. La crate futures est la référence officielle d’expérimentation de code asynchrone, et c’est en fait là que le trait Future a été conçu pour la première fois. Tokio est le moteur d’exécution asynchrone le plus largement utilisé en Rust aujourd’hui, en particulier pour les applications Web. Il y a d’autres moteurs d’exécution qui sont très bien, et qui pourraient bien mieux convenir à vos besoins. Nous utilisons la crate tokio sous le capot pour trpl car elle est bien testée et largement utilisée.

Dans certains cas, trpl renomme ou emballe les APIs originales de manière à ce que vous restiez focalisé sur les détails correspondant à ce chapitre. Si vous voulez comprendre ce que fait la crate, nous vous encourageons à aller voir son code source. Vous pourrez voir de quelle crate chaque re-export provient, et nous avons laissé d’abondants commentaires expliquant ce que fait la crate.

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

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

Nous pouvons maintenant utiliser les divers éléments fournis par trpl pour écrire notre premier programme asynchrone. Nous allons construire un petit outil en ligne de commande qui récupère deux pages Web, récupère l’élément <title> de chacune, puis écrit le titre de la première page dont le traitement se terminera en premier.

Définition de la fonction page_titre

Commençons par écrire une fonction qui prend une adresse URL en paramètre, y envoie une requête, et renvoie le texte de l’élément <title> (voir l’encart 17-1).

Filename: src/main.rs
extern crate trpl; // requis par test mdbook

fn main() {
    // TODO: on mettra ça plus tard !
}

use trpl::Html;

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

Nous commençons par définir une fonction nommée page_titre et la marquons avec le mot-clé async. Nous utilisons ensuite la fonction trpl::get pour récupérer l’URL qui lui est passée et ajoutons le mot-clé await pour attendre la réponse. Pour obtenir le texte de la reponse, nous appelons sa méthode text et l’attendons de 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, laquelle contiendra les en-têtes HTTP, les cookies, etc., et qui peut être transmise séparément du corps de la réponse. En particulier si le corps est très grand, son arrivée complète peut prendre du temps. Puisque nous devons attendre que l’intégralité de la réponse arrive, la méthode text est aussi asynchrone.

Nous devons explicitement attendre ces deux futures, car les futures sont paresseuses, en Rust : elles ne font rien jusqu’à ce qu’on leur demande d’agir avec le mot-clé await (en fait, Rust indiquera un avertissement du compilateur si vous n’utilisez pas une future). Ceci pourrait vous rappeler la discussion à propos des itérateurs dans la section “Traiter une série d’éléments avec des itérateurs”“” du chapitre 13. Les itérateurs ne font rien jusqu’à ce que vous appeliez leur méthode next — soit directement, soit en utilisant des boucles for ou des méthodes comme map qui utilisent next sous le capot. De même, les futures ne font rien à moins que vous ne leur demandiez explicitement d’agir. Ce caractère paresseux permet à Rust d’éviter d’exécuter du code asynchrone jusqu’à ce qu’il soit réellement nécessaire.

Note : ce comportement est différent de celui que nous avons vu en utilisant thread::spawn dans la section “Créer une nouvelle tâche avec spawn” du chapitre 16, dans laquelle la fermeture que nous passions à une autre tâche commençait à s’exécuter immédiatement. C’est également une approche différente de celle adoptée par de nombreux autres langages en matière d’asynchronisme. Mais il est important pour Rust de pouvoir fournir ses garanties de performance, exactement de la même manière que pour les itérateurs.

Une fois que nous avons reponse_texte, nous pouvons l’analyser pour obtenir une instance du type Html en utilisant Html::parse. Au lieu d’une chaîne de caractères brute, nous avons désormais un type de données que nous pouvons utiliser pour manipuler le code HTML comme une structure de données plus riche. Nous pouvons notamment 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 en existe un. Comme il peut n’y avoir aucun élément correspondant, select_first renvoie un Option<ElementRef>. Enfin, nous utilisons la méthode Option::map, laquelle nous permet de travailler avec l’élément contenu dans l’Option s’il est présent, et de ne rien faire s’il ne l’est pas (nous aurions aussi pu ici utiliser une expression match, mais map est plus idiomatique). Dans le corps de la fonction que nous fournissonsà map, nous appelons inner_html sur le titre pour récupérer son contenu, qui est un String. Et au final, nous avons un Option<String>.

Notez que le mot-clé Rust await se positionne après l’expression que vous attendez et non pas avant. Autrement dit, il s’agit d’un mot-clé postfix. Il se peut que cela diffère de ce à quoi vous êtes habitués si vous avez utilisé la programmation asynchrone dans d’autres langages, mais en Rust cela donne des enchaînements de méthodes qui sont bien plus agréables à utiliser. Conséquemment, nous pourrions modifier le corps de page_titre pour enchaîner les appels de fonction trpl::get et text en intercalant await entre elles, comme le montre l’encart 17-2.

Filename: src/main.rs
extern crate trpl; // requis par test mdbook

use trpl::Html;

fn main() {
    // TODO: on mettra ça plus tard !
}

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

Avec tout cela, nous avons réussi à écrire notre première fonction asynchrone ! Avant d’ajouter du code dans main pour l’appeler, voyons plus en détail ce que nous avons écrit et ce que cela signifie.

Quand Rust voit un bloc marqué avec le mot-clé async, il le compile 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 compile en une fonction non asynchrone dont le corps est un bloc asynchrone. Le type de retour d’une fonction asynchrone est le type du type de données anonyme que le compilateur crée pour ce bloc asynchrone.

Conséquemment, écrire async fn revient à écrire une fonction qui renvoie une future du type de retour. Pour le compilateur, une définition de fonction telle que async fn page_titre de l’encart 17-1 est à peu près équivalente à une fonction non asynchrone définie ainsi :

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

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

Passons en revue chaque partie de la version modifiée :

  • Elle utilise la syntaxe impl Trait dont nous avons parlé au chapitre 10, dans la section “Utilisation des traits comme paramètres”.
  • La valeur renvoyée implémente le trait Future avec un type associé Output. Notez 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_titre.
  • Tout le code appelé dans le corps de la fonction originale est empaqueté dans un bloc async move. Rappelez-vous que les blocs sont des expressions. Ce bloc tout entier est l’expression renvoyée de la fonction.
  • Ce bloc asynchrone produit une valeur avec le type Option<String>, comme on vient de le voir. Cette valeur correspond au type Output` du type de retour. C’est exactement comme pour les autres blocs que vous avez vus.
  • Le nouveau corps de fonction est un bloc async move à cause de la manière dont il utilise le paramètre url (nous reviendrons plus en détail sur la différence entre async et async move plus loin dans ce chapitre).

Nous pouvons maintenant appeler page_titre dans main.

Exécution d’une fonction asynchrone avec un moteur d’exécution

Pour commencer, nous allons récupérer le titre pour une seule page, montrée dans l’encart 17-3. Malheureusement, ce code ne se compile pas encore.

Filename: src/main.rs
extern crate trpl; // requis par test mdbook

use trpl::Html;

async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let url = &args[1];
    match page_titre(url).await {
        Some(titre) => println!("Le titre de {url} était {titre}"),
        None => println!("{url} n'avait pas de titre"),
    }
}

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

Nous suivons la même méthode que nous avions utilisée pour récupérer les arguments de la ligne de commande dans la section “Récupérer les arguments de la ligne de commande” du chapitre 12. Ensuite, on passe l’URL en argument à `page_titre

Le seul endroit où nous pouvons utiliser le mot-clé await est dans des fonctions asynchrones ou dans des blocs, 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 comme async est que le code asynchrone a besoin d’un moteur d’exécution : une crate Rust qui gère les détails d’éxécution de code asynchrone. La fonction main d’un programme peut initialiser un moteur d’exécution, mais elle n’est pas un moteur d’exécution en elle-même (nous verrons sous peu pourquoi). Chaque programme Rust qui exécute du code asynchrone a au moins un endroit où il met en place un moteur d’exécution qui exécutera les futures.

La plupart des langages qui supportent la programmation asynchrone viennent avec un moteur d’exécution, mais ce n’est pas le cas de Rust. À la place, il y a de nombreux moteurs d’exécution asynchrones différents, chacun d’entre eux faisant différents compromis, adaptés au cas d’utilisation qu’il cible. Par exemple, un serveur web à haut débit doté de nombreux cœurs de processeur et d’une grande quantité de mémoire vive a des besoins très différents d’un microcontrôleur avec un simple cœur, une faible quantité de mémoire vive, et pas de capacité d’allocation de mémoire sur le tas. Les crates qui fournissent ces moteurs d’exécution proposent souvent des versions asynchrones de fonctionnalités courantes comme les entrées/sorties de fichiers ou réseau.

Ici, et tout au long du reste de ce chapitre, nous allons utiliser la fonction block_on de la crate trpl, qui prend une future en argument et bloque la tâche courante jusqu’à ce que cette future se déroule jusqu’à son terme. En coulisses, l’appel de block_on met en place un moteur d’exécution qui utilise la crate tokio, laquelle sert à exécuter la future qui est transmise (le comportement block_on de la crate trpl est similaire à celui des fonctions block_on d’autres crates). Une fois que la future termine son exécution, block_on renvoie la valeur que la future aura produit.

Nous pourrions passer la future renvoyée par page_titre directement à block_on et, une fois l’exécution terminée, nous pourrions effectuer une comparaison avec l’Option<String> obtenu, comme nous avons tenté de le faire dans l’encart 17-3. Cependant, pour la plupart des exemples de ce chapitre (et pour la plupart des codes asynchrones dans la pratique), nous ferons plus qu’un simple appel de fonction asynchrone ; à la place, nous transmettrons donc un bloc async et attendrons explicitement le résultat de l’appel à page_titre, comme dans l’encart 17-4.

Filename: src/main.rs
extern crate trpl; // requis par test mdbook

use trpl::Html;

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

    trpl::block_on(async {
        let url = &args[1];
        match page_titre(url).await {
            Some(titre) => println!("Le titre de {url} était {titre}"),
            None => println!("{url} n'avait pas de titre"),
        }
    })
}

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

Si nous lançons ce code, nous obtenons le comportement auquel nous nous attendions 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

Eh bien… nous avons finalement du code asynchrone qui fonctionne ! Mais avant d’ajouter du code pour faire la course entre deux sites, focalisons-nous brièvement sur la manière dont les futures fonctionnent.

Chaque point d’attente (NDT : await point) — c’est à dire chaque endroit où le code comporte le mot-clé await — représente un endroit où le contrôle est rendu au moteur d’exécution. Pour que cela fonctionne, Rust doit garder une trace de l’état impliqué dans le bloc asynchrone, de telle sorte que le moteur d’exécution puisse lancer une autre tâche, puis revenir lorsqu’il est de nouveau prêt à avancer sur la première tâche. Il s’agit d’une machine à états invisible, comme si vous aviez écrit une énumération comme celle-ci pour enregistrer le contexte actuel à chaque point d’attente.

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

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

Écrire à la main le code pour basculer entre chaque état serait très fastidieux et source d’erreurs, cependant, particulièrement quand, plus tard, vous aurez besoin d’ajouter au code plus de fonctionnalités et plus d’états. Heureusement, le compilateur Rust crée et gère automatiquement les structures de données de la machine à états pour le code asynchrone. Les règles habituelles d’emprunt et de possession concernant les structures de données restent toutes en vigueur et, heureusement, le compilateur se charge également de les vérifier pour nous et fournit des messages d’erreur utiles. Nous aborderons quelques-uns d’entre eux plus loin dans ce chapitre.

En fin de compte, quelque chose doit exécuter cette machine à états, et ce quelque chose est un moteur d’exécution (c’est pourquoi vous pourriez tomber sur des références aux exécuteurs (NDT : executors) si vous vous intéressez aux moteurs d’exécution : un exécuteur est la partie d’un moteur d’exécution chargée d’exécuter du code asynchrone).

Vous pouvez maintenant voir la raison pour laquelle le compilateur nous a empêché de faire de main une fonction asynchrone, dans l’encart 17-3. Si main était une fonction asynchrone, il faudrait que quelque chose d’autre gère la machine à états pour la future renvoyée par main ; mais il se trouve que main est le point de départ du programme ! À la place, nous avons appelé la fonction trpl::block_on dans main pour mettre en place un moteur d’exécution et exécuter la future renvoyée par le bloc async jusqu’à ce qu’il soit terminé.

Note : certains moteurs d’exécution fournissent des macros de manière à ce que vous puissiez écrire une fonction main asynchrone. Ces macros réécrivent async fn main() { ... } pour en faire une fn main normale, ce qui revient au même que ce que nous avons fait à la main dans l’encart 17-4 : appeler une fonction qui renvoie une future jusqu’à son terme, à la manière de trpl::block_on.

Assemblons maintenant toutes ces pièces ensemble et voyons comment écire du code concurrent.

Course entre deux URLs en concurrence

Dans l’encart 17-5, nous avons appelé page_titre avec deux URLs passées depuis la ligne de commande et les avons fait faire la course en sélectionnant la première future qui termine en premier.

Filename: src/main.rs
extern crate trpl; // requis par test mdbook

use trpl::{Either, Html};

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

    trpl::block_on(async {
        let titre_fut_1 = page_titre(&args[1]);
        let titre_fut_2 = page_titre(&args[2]);

        let (url, titre_possible) =
            match trpl::select(titre_fut_1, titre_fut_2).await {
                Either::Left(left) => left,
                Either::Right(right) => right,
            };

        println!("{url} est arrivé en premier");
        match titre_possible {
            Some(titre) => println!("Le titre de la page était : '{titre}'"),
            None => println!("Il n'y avait pas de titre."),
        }
    })
}

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

Nous commençons par appeler page_titre pour chacune des URLs fournies par l’utilisateur. Nous sauvegardons les futures obtenues sous les noms titre_fut_1 et titre_fut_2. N’oubliez pas que celles-ci ne font encore rien, car les futures sont paresseuses et que nous ne les avons pas encore attendues. Nous transmettons les futures à trpl::select, qui renvoie une valeur indiquant laquelle des futures qui lui ont été transmises a terminé la première.

Note : sous le capot, trpl::select est construit par-dessus un select plus général, défini dans la crate futures. La fonction select de la crate futures peut faire un grand nombre de choses que la fonction trpl::select ne peut faire, mais elle a aussi des complexités supplémentaires que nous pouvons ignorer pour le moment.

Chaque future peut tout à fait “gagner,” il n’est donc pas à propos de renvoyer un Result. À la place, trpl::select renvoie un type que nous n’avons jusqu’ici jamais vu, trpl::Either. Le type Either est un peu similaire à un Result, dans le sens où il comporte deux cas. Mais contrairement à Result, though, il n’y a aucune notion de succès ou d’échec intégrée à Either. À la place, il utilise Left et Right pour indiquer l’un ou l’autre cas :

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

La fonction select renvoie Left avec la sortie de la future si le premier argument l’emporte, et Right avec la sortie de la deuxième future si celle-ci l’emporte. Ceci correspond à l’ordre dans lequel les arguments apparaissent lors de l’appel de la fonction : le premier argument est situé à gauche du second argument.

Nous mettons aussi à jour page_titre pour renvoyer la même URL que celle passée à la fonction. Ainsi, si la page qui se termine en premier n’a pas de <title> que nous pouvons résoudre, nous pouvons toujours écrire un message sensé. Avec cette information de disponible, nous empaquetons le tout en mettant à jour notre sortie de println! pour indiquer à la fois quelle URL a terminé en premier et, le cas échéant, ce qu’est le <title> de la page web à cette URL.

Vous venez de construire un petit scraper web fonctionnel ! Choisissez quelques URLs et lancez l’outil en ligne de commande. Vous constaterez peut-être que certains sites sont systématiquement plus rapides que d’autres, alors que dans d’autres cas, le site le plus rapide varie d’une exécution à l’autre. Mais surtout, vous avez acquis les bases de l’utilisation des futures, ce qui nous permet maintenant d’approfondir les possibilités offertes par la programmation asynchrone.