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

Céder le contrôle au runtime

Rappelez-vous dans la section « Notre premier programme async » du chapitre 17, nous avons utilisé trpl::block_on pour attendre qu’un futur unique se terminé en pilotant l’exécution manuellement.

Cela signifie que si vous effectuez beaucoup de travail dans un bloc async sans point d’attente, cette future bloquera toutes les autres futures et les empêchera de progresser. Vous entendrez parfois parler d’une future qui affame les autres futures. Dans certains cas, cela peut ne pas être grave. Cependant, si vous effectuez une configuration coûteuse ou un travail de longue durée, ou si vous avez une future qui continuera à effectuer une tâche particulière indéfiniment, vous devrez réfléchir à quand et où rendre le contrôle au runtime.

Simulons une opération de longue durée pour illustrer le problème d’affamement, puis explorons comment le résoudre. L’encart 17-14 introduit une fonction slow.

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

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        // We will call `slow` here later
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
Listing 17-14: Using thread::sleep to simulate slow operations

Ce code utilise std::thread::sleep au lieu de trpl::sleep pour que l’appel à slow bloque le thread courant pendant un certain nombre de millisecondes. Nous pouvons utiliser slow pour représenter des opérations du monde réel qui sont à la fois longues et bloquantes.

Dans l’encart 17-15, nous utilisons slow pour émuler ce type de travail limité par le CPU dans une paire de futures.

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

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        let a = async {
            println!("'a' started.");
            slow("a", 30);
            slow("a", 10);
            slow("a", 20);
            trpl::sleep(Duration::from_millis(50)).await;
            println!("'a' finished.");
        };

        let b = async {
            println!("'b' started.");
            slow("b", 75);
            slow("b", 10);
            slow("b", 15);
            slow("b", 350);
            trpl::sleep(Duration::from_millis(50)).await;
            println!("'b' finished.");
        };

        trpl::select(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
Listing 17-15: Calling the slow function to simulate slow operations

Chaque future ne rend le contrôle au runtime qu’après avoir effectué une série d’opérations lentes. Si vous exécutez ce code, vous verrez cette sortie :

'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.

Comme dans l’encart 17-5 où nous avons utilisé trpl::select pour mettre en compétition des futures récupérant deux URL, select se terminé toujours dès que a est terminée. Il n’y a cependant pas d’entrelacement entre les appels à slow dans les deux futures. La future a fait tout son travail jusqu’à ce que l’appel trpl::sleep soit attendu, puis la future b fait tout son travail jusqu’à ce que son propre appel trpl::sleep soit attendu, et enfin la future a se terminé. Pour permettre aux deux futures de progresser entre leurs tâches lentes, nous avons besoin de points d’attente pour pouvoir rendre le contrôle au runtime. Cela signifie que nous avons besoin de quelque chose que nous pouvons attendre !

Nous pouvons déjà voir ce type de transfert se produire dans l’encart 17-15 : si nous supprimions le trpl::sleep à la fin de la future a, elle se terminerait sans que la future b ne s’exécute du tout. Essayons d’utiliser la fonction trpl::sleep comme point de départ pour laisser les opérations alterner leur progression, comme montré dans l’encart 17-16.

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

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        let one_ms = Duration::from_millis(1);

        let a = async {
            println!("'a' started.");
            slow("a", 30);
            trpl::sleep(one_ms).await;
            slow("a", 10);
            trpl::sleep(one_ms).await;
            slow("a", 20);
            trpl::sleep(one_ms).await;
            println!("'a' finished.");
        };

        let b = async {
            println!("'b' started.");
            slow("b", 75);
            trpl::sleep(one_ms).await;
            slow("b", 10);
            trpl::sleep(one_ms).await;
            slow("b", 15);
            trpl::sleep(one_ms).await;
            slow("b", 350);
            trpl::sleep(one_ms).await;
            println!("'b' finished.");
        };

        trpl::select(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
Listing 17-16: Using trpl::sleep to let operations switch off making progress

Nous avons ajouté des appels trpl::sleep avec des points d’attente entre chaque appel à slow. Maintenant le travail des deux futures est entrelacé :

'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.

La future a s’exécute encore un peu avant de passer le contrôle à b, car elle appelle slow avant d’appeler trpl::sleep, mais après cela les futures alternent à chaque fois que l’une d’elles atteint un point d’attente. Dans ce cas, nous avons fait cela après chaque appel à slow, mais nous pourrions découper le travail de la manière qui à le plus de sens pour nous.

Nous ne voulons pas vraiment dormir ici : nous voulons progresser aussi vite que possible. Nous avons juste besoin de rendre le contrôle au runtime. Nous pouvons le faire directement en utilisant la fonction trpl::yield_now. Dans l’encart 17-17, nous remplaçons tous ces appels trpl::sleep par trpl::yield_now.

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

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        let a = async {
            println!("'a' started.");
            slow("a", 30);
            trpl::yield_now().await;
            slow("a", 10);
            trpl::yield_now().await;
            slow("a", 20);
            trpl::yield_now().await;
            println!("'a' finished.");
        };

        let b = async {
            println!("'b' started.");
            slow("b", 75);
            trpl::yield_now().await;
            slow("b", 10);
            trpl::yield_now().await;
            slow("b", 15);
            trpl::yield_now().await;
            slow("b", 350);
            trpl::yield_now().await;
            println!("'b' finished.");
        };

        trpl::select(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
Listing 17-17: Using yield_now to let operations switch off making progress

Ce code est à la fois plus clair quant à l’intention réelle et peut être significativement plus rapide que l’utilisation de sleep, car les minuteries comme celle utilisée par sleep ont souvent des limites sur leur granularité. La version de sleep que nous utilisons, par exemple, dormira toujours pendant au moins une milliseconde, même si nous lui passons une Duration d’une nanoseconde. Encore une fois, les ordinateurs modernes sont rapides : ils peuvent faire beaucoup en une milliseconde !

Cela signifie que l’async peut être utile même pour les tâches limitées par le calcul, en fonction de ce que fait d’autre votre programme, car il fournit un outil utile pour structurer les relations entre différentes parties du programme (mais au prix du surcoût de la machine à états async). C’est une forme de multitâche coopératif, où chaque future à le pouvoir de déterminer quand elle cède le contrôle via les points d’attente. Chaque future a donc aussi la responsabilité d’éviter de bloquer trop longtemps. Dans certains systèmes d’exploitation embarqués basés sur Rust, c’est le seul type de multitâche !

Dans le code du monde réel, vous n’alternerez bien sûr pas habituellement les appels de fonctions avec des points d’attente à chaque ligne. Bien que céder le contrôle de cette manière soit relativement peu coûteux, ce n’est pas gratuit. Dans de nombreux cas, essayer de découper une tâche limitée par le calcul pourrait la rendre significativement plus lente, donc parfois il est préférable pour les performances globales de laisser une opération bloquer brièvement. Mesurez toujours pour voir quels sont les véritables goulots d’étranglement de performance de votre code. La dynamique sous-jacente est cependant importante à garder à l’esprit, si vous constatez beaucoup de travail s’effectuant en série alors que vous vous attendiez à ce qu’il se fasse de manière concurrente !

Construire nos propres abstractions async

Nous pouvons aussi composer des futures ensemble pour créer de nouveaux motifs. Par exemple, nous pouvons construire une fonction timeout avec les blocs de construction async que nous avons déjà. Quand nous aurons terminé, le résultat sera un autre bloc de construction que nous pourrons utiliser pour créer encore plus d’abstractions async.

L’encart 17-18 montre comment nous nous attendrions à ce que ce timeout fonctionne avec une future lente.

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

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let slow = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Finally finished"
        };

        match timeout(slow, Duration::from_secs(2)).await {
            Ok(message) => println!("Succeeded with '{message}'"),
            Err(duration) => {
                println!("Failed after {} seconds", duration.as_secs())
            }
        }
    });
}
Listing 17-18: Using our imagined timeout to run a slow operation with a time limit

Implémentons cela ! Pour commencer, réfléchissons à l’API de timeout :

  • Elle doit être une fonction async elle-même pour que nous puissions l’attendre.
  • Son premier paramètre devrait être une future à exécuter. Nous pouvons la rendre générique pour qu’elle fonctionne avec n’importe quelle future.
  • Son deuxième paramètre sera le temps maximum d’attente. Si nous utilisons une Duration, ce sera facile de la passer à trpl::sleep.
  • Elle devrait retourner un Result. Si la future se terminé avec succès, le Result sera Ok avec la valeur produite par la future. Si le timeout s’écoule en premier, le Result sera Err avec la durée pendant laquelle le timeout a attendu.

L’encart 17-19 montre cette déclaration.

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

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let slow = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Finally finished"
        };

        match timeout(slow, Duration::from_secs(2)).await {
            Ok(message) => println!("Succeeded with '{message}'"),
            Err(duration) => {
                println!("Failed after {} seconds", duration.as_secs())
            }
        }
    });
}

async fn timeout<F: Future>(
    future_to_try: F,
    max_time: Duration,
) -> Result<F::Output, Duration> {
    // Here is where our implémentation will go!
}
Listing 17-19: Defining the signature of timeout

Cela satisfait nos objectifs pour les types. Maintenant, réfléchissons au comportement dont nous avons besoin : nous voulons mettre en compétition la future passée en paramètre contre la durée. Nous pouvons utiliser trpl::sleep pour créer une future de minuterie à partir de la durée, et utiliser trpl::select pour exécuter cette minuterie avec la future que l’appelant passe.

Dans l’encart 17-20, nous implémentons timeout en faisant un match sur le résultat de l’attente de trpl::select.

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

use std::time::Duration;

use trpl::Either;

// --snip--

fn main() {
    trpl::block_on(async {
        let slow = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Finally finished"
        };

        match timeout(slow, Duration::from_secs(2)).await {
            Ok(message) => println!("Succeeded with '{message}'"),
            Err(duration) => {
                println!("Failed after {} seconds", duration.as_secs())
            }
        }
    });
}

async fn timeout<F: Future>(
    future_to_try: F,
    max_time: Duration,
) -> Result<F::Output, Duration> {
    match trpl::select(future_to_try, trpl::sleep(max_time)).await {
        Either::Left(output) => Ok(output),
        Either::Right(_) => Err(max_time),
    }
}
Listing 17-20: Defining timeout with select and sleep

L’implémentation de trpl::select n’est pas équitable : elle interroge toujours les arguments dans l’ordre dans lequel ils sont passés (d’autres implémentations de select choisiront aléatoirement quel argument interroger en premier). Ainsi, nous passons future_to_try à select en premier pour qu’elle ait une chance de se terminer même si max_time est une durée très courte. Si future_to_try se terminé en premier, select retournera Left avec la sortie de future_to_try. Si timer se terminé en premier, select retournera Right avec la sortie () de la minuterie.

Si future_to_try réussit et que nous obtenons un Left(output), nous retournons Ok(output). Si la minuterie de sommeil s’écoule à la place et que nous obtenons un Right(()), nous ignorons le () avec _ et retournons Err(max_time) à la place.

Avec cela, nous avons un timeout fonctionnel construit à partir de deux autres aides async. Si nous exécutons notre code, il affichera le mode d’échec après le timeout : text Failed after 2 seconds

Failed after 2 seconds

Comme les futures se composent avec d’autres futures, vous pouvez construire des outils vraiment puissants en utilisant de petits blocs de construction async. Par exemple, vous pouvez utiliser cette même approche pour combiner des timeouts avec des tentatives de reprise, et à leur tour les utiliser avec des opérations comme des appels réseau (comme ceux de l’encart 17-5).

En pratique, vous travaillerez généralement directement avec async et await, et secondairement avec des fonctions comme select et des macros comme la macro join! pour contrôler comment les futures les plus extérieures sont exécutées.

Nous avons maintenant vu plusieurs façons de travailler avec plusieurs futures en même temps. Ensuite, nous verrons comment nous pouvons travailler avec plusieurs futures en séquence au fil du temps avec les streams.