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