🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Vous pouvez contribuer à l'amélioration de cette page sur sa Pull Request.

async et await

Dans le premier chapitre, nous avons présenté async et await. Ce nouveau chapitre va aborder plus en détails async et await, en expliquant comment il fonctionne et comment le code async se distingue des programmes Rust traditionnels.

async et await sont des mot-clés spécifiques de la syntaxe Rust qui permet de transférer le contrôle du processus en cours plutôt que de le bloquer, ce qui permet à un autre code de progresser pendant que nous attendons que cette opération se termine.

Il y a deux principaux moyens d'utiliser async : async fn et les blocs async. Chacun retourne une valeur qui implémente le trait Future :


// `alpha()` retourne un type qui implémente `Future<Output = u8>`.
// `alpha().await` va retourner une valeur de type `u8`.
async fn alpha() -> u8 { 5 }

fn beta() -> impl Future<Output = u8> {
    // Ce bloc `async` va retourner un type qui implémente
    // `Future<Output = u8>`.
    async {
        let x: u8 = alpha().await;
        x + 5
    }
}

Comme nous l'avons vu dans le premier chapitre, les corps des async et des autres futures sont passifs : ils ne font rien jusqu'à ce qu'ils soient exécutés. La façon la plus courante d'exécuter une Future est d'utiliser await sur elle. Lorsque await est utilisé sur une Future, il va tenter de l'exécuter jusqu'à sa fin. Si la Future est bloquée, il va transférer le contrôle du processus en cours. Lorsqu'une progression pourra être effectuée à nouveau, la Future va être récupérée par l'exécuteur et va continuer son exécution, ce qui permettra à terme au await de se résoudre.

Les durées de vie async

Contrairement aux fonctions traditionnelles, les async fn qui utilisent des références ou d'autres arguments non static vont retourner une Future qui est contrainte par la durée de vie des arguments :

// Cette fonction :
async fn alpha(x: &u8) -> u8 { *x }

// ... est équivalente à cette fonction :
fn alpha_enrichi<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
    async move { *x }
}

Cela signifie que l'on doit utiliser await sur la future retournée d'une async fn uniquement pendant que ses arguments non static sont toujours en vigueur. Dans le cas courant où on utilise await sur la future immédiatement après avoir appelé la fonction (comme avec alpha(&x).await), ce n'est pas un problème. Cependant, si on stocke la future ou si on l'envoie à une autre tâche ou processus, cela peut devenir un problème.

Un contournement courant pour utiliser une async fn avec des références en argument afin qu'elle retourne une future 'static est d'envelopper à l'intérieur d'un bloc async les arguments utilisés pour l'appel à la async fn :

fn incorrect() -> impl Future<Output = u8> {
    let x = 5;
    emprunter_x(&x) // ERREUR : `x` ne vit pas suffisamment longtemps
}

fn correct() -> impl Future<Output = u8> {
    async {
        let x = 5;
        emprunter_x(&x).await
    }
}

En déplaçant l'argument dans le bloc async, nous avons étendu sa durée de vie à celle de cette Future qui est retournée suite à l'appel à correct.

async move

Les blocs et fermetures asyncautorisent l'utilisation du mot-clé move, comme les fermetures synchrones. Un bloc async move va prendre possession des variables qu'il utilise, leur permettant de survivre à l'extérieur de la portée actuelle, mais par conséquent qui empêche de partager ces variables avec un autre code :

/// blocs `async` :
///
/// Plusieurs blocs `async` différents peuvent accéder à la même variable
/// locale tant qu'elles sont exécutées dans la portée de la variable
async fn blocs() {
    let ma_chaine = "alpha".to_string();

    let premiere_future = async {
        // ...
        println!("{ma_chaine}");
    };

    let seconde_future = async {
        // ...
        println!("{ma_chaine}");
    };

    // Exécute les deux futures jusqu'à leur fin, ce qui affichera
    // deux fois "alpha" :
    let ((), ()) = futures::join!(premiere_future, seconde_future);
}

/// blocs `async move` :
///
/// Un seul bloc `async move` peut avoir accès à la même variable capturée,
/// puisque qu'elles sont déplacées dans la `Future` générée par le bloc
/// `async move`.
/// Cependant, cela permet d'étendre la portée de la `Future` en dehors de
/// celle de la variable :
fn bloc_avec_move() -> impl Future<Output = ()> {
    let ma_chaine = "alpha".to_string();
    async move {
        // ...
        println!("{ma_chaine}");
    }
}

Utiliser await avec un exécuteur multi-processus

Remarquez que lorsque vous utilisez un exécuteur de Future multi-processus, une Future peut être déplacée entre les processus, donc toutes les variables utilisées dans les corps des async doivent pouvoir aussi être déplacés entre des processus, car n'importe quel await peut potentiellement basculer sur un autre processus.

Cela signifie que ce n'est sûr d'utiliser Rc, &RefCell ou tout autre type qui n'implémente pas le trait Send, y compris les références à des types qui n'implémente pas le trait Sync.

(Remarque : il reste possible d'utiliser ces types du moment qu'ils ne sont pas dans la portée d'un appel à await)

Pour la même raison, ce n'est pas une bonne idée de maintenir un verrou traditionnel, qui ne se préoccupe pas des futures, dans un await, car cela peut provoquer le blocage du groupe de processus : une tâche peut poser le verrou, attendre grâce à await et transférer le contrôle à l'exécuteur, qui va permettre à une autre tâche de vouloir poser le verrou et cela va causer un interblocage. Pour éviter cela, utilisez le Mutex dans futures::lock plutôt que celui dans std::sync.