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