Utiliser les threads pour exécuter du code simultanément

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Utiliser les threads pour exécuter du code simultanément. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous explorerons le concept de threads en programmation et la manière dont ils peuvent être utilisés pour exécuter du code simultanément, améliorant ainsi les performances mais ajoutant de la complexité et des problèmes potentiels tels que les conditions de course, les verrous morts et des bogues difficiles à reproduire.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100437{{"Utiliser les threads pour exécuter du code simultanément"}} rust/for_loop -.-> lab-100437{{"Utiliser les threads pour exécuter du code simultanément"}} rust/function_syntax -.-> lab-100437{{"Utiliser les threads pour exécuter du code simultanément"}} rust/expressions_statements -.-> lab-100437{{"Utiliser les threads pour exécuter du code simultanément"}} rust/lifetime_specifiers -.-> lab-100437{{"Utiliser les threads pour exécuter du code simultanément"}} rust/method_syntax -.-> lab-100437{{"Utiliser les threads pour exécuter du code simultanément"}} end

Utiliser les threads pour exécuter du code simultanément

Dans la plupart des systèmes d'exploitation actuels, le code d'un programme exécuté est exécuté dans un processus, et le système d'exploitation gérera plusieurs processus en même temps. Dans un programme, vous pouvez également avoir des parties indépendantes qui s'exécutent simultanément. Les fonctionnalités qui exécutent ces parties indépendantes sont appelées threads. Par exemple, un serveur web pourrait avoir plusieurs threads de sorte qu'il puisse répondre à plusieurs requêtes en même temps.

Diviser le calcul dans votre programme en plusieurs threads pour exécuter plusieurs tâches en même temps peut améliorer les performances, mais cela ajoute également de la complexité. Parce que les threads peuvent s'exécuter simultanément, il n'y a pas de garantie inhérente sur l'ordre dans lequel les parties de votre code sur différents threads seront exécutées. Cela peut entraîner des problèmes tels que :

  • Les conditions de course, où les threads accèdent à des données ou des ressources dans un ordre incohérent
  • Les verrous morts, où deux threads s'attendent mutuellement, empêchant les deux threads de continuer
  • Des bogues qui ne se produisent que dans certaines situations et sont difficiles à reproduire et à corriger de manière fiable

Rust tente d'atténuer les effets négatifs de l'utilisation de threads, mais la programmation dans un contexte multithreadé nécessite toujours une réflexion attentive et exige une structure de code différente de celle des programmes s'exécutant dans un seul thread.

Les langages de programmation implémentent les threads de quelques manières différentes, et de nombreux systèmes d'exploitation fournissent une API que le langage peut appeler pour créer de nouveaux threads. La bibliothèque standard de Rust utilise un modèle 1:1 d'implémentation de threads, selon lequel un programme utilise un thread d'exploitation par un thread de langage. Il existe des crânes qui implémentent d'autres modèles de threading qui font des compromis différents par rapport au modèle 1:1.

Création d'un nouveau thread avec spawn

Pour créer un nouveau thread, nous appelons la fonction thread::spawn et lui passons une closure (nous avons parlé des closures au chapitre 13) contenant le code que nous voulons exécuter dans le nouveau thread. L'exemple de la Liste 16-1 affiche du texte dans le thread principal et d'autres textes dans un nouveau thread.

Nom de fichier : src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}

Liste 16-1 : Création d'un nouveau thread pour afficher une chose tandis que le thread principal affiche autre chose

Notez que lorsque le thread principal d'un programme Rust se termine, tous les threads lancés sont arrêtés, que ce soit s'ils ont fini d'exécuter ou non. La sortie de ce programme peut être un peu différente à chaque exécution, mais elle ressemblera à ceci :

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

Les appels à thread::sleep forcent un thread à arrêter son exécution pendant une courte durée, permettant à un autre thread de s'exécuter. Les threads prendront probablement la relève, mais cela n'est pas garanti : cela dépend de la façon dont votre système d'exploitation programme les threads. Dans cette exécution, le thread principal a affiché en premier, même si l'instruction d'affichage du thread lancé apparaît en premier dans le code. Et même si nous avons dit au thread lancé d'afficher jusqu'à ce que i soit égal à 9, il n'est arrivé qu'à 5 avant que le thread principal ne se termine.

Si vous exécutez ce code et ne voyez que la sortie du thread principal, ou si vous ne voyez pas d'emboîtement, essayez d'augmenter les nombres dans les plages pour créer plus de possibilités pour le système d'exploitation de basculer entre les threads.

Attendre que tous les threads se terminent en utilisant des join handles

Le code de la Liste 16-1 arrête non seulement le thread lancé prématurément la plupart du temps en raison de la fin du thread principal, mais également, puisque l'ordre d'exécution des threads n'est pas garanti, nous ne pouvons pas non plus garantir que le thread lancé va même s'exécuter du tout!

Nous pouvons résoudre le problème du non-exécution ou de la terminaison prématurée du thread lancé en enregistrant la valeur de retour de thread::spawn dans une variable. Le type de retour de thread::spawn est JoinHandle<T>. Un JoinHandle<T> est une valeur propriétaire qui, lorsqu'on appelle la méthode join dessus, attendra que son thread se termine. La Liste 16-2 montre comment utiliser le JoinHandle<T> du thread que nous avons créé dans la Liste 16-1 et appeler join pour vous assurer que le thread lancé se termine avant que main ne sorte.

Nom de fichier : src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

Liste 16-2 : Enregistrer un JoinHandle<T> de thread::spawn pour garantir que le thread est exécuté jusqu'à la fin

Appeler join sur le handle bloque le thread actuellement en cours d'exécution jusqu'à ce que le thread représenté par le handle se termine. Bloquer un thread signifie que ce thread est empêché de travailler ou de sortir. Comme nous avons placé l'appel à join après la boucle for du thread principal, exécuter la Liste 16-2 devrait produire une sortie similaire à celle-ci :

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

Les deux threads continuent de s'alterner, mais le thread principal attend en raison de l'appel à handle.join() et ne se termine pas avant que le thread lancé ne soit terminé.

Mais voyons ce qui se passe lorsque nous plaçons handle.join() avant la boucle for dans main, comme ceci :

Nom de fichier : src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}

Le thread principal attendra que le thread lancé se termine puis exécutera sa boucle for, de sorte que la sortie ne sera plus intercalée, comme le montre ici :

hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

De petits détails, comme le lieu où join est appelé, peuvent affecter si vos threads s'exécutent en même temps ou non.

Utiliser des closures move avec des threads

Nous utiliserons souvent le mot-clé move avec les closures passées à thread::spawn car la closure prendra alors la propriété des valeurs qu'elle utilise de l'environnement, transférant ainsi la propriété de ces valeurs d'un thread à l'autre. Dans "Capturer l'environnement avec les closures", nous avons discuté de move dans le contexte des closures. Maintenant, nous nous concentrerons plus sur l'interaction entre move et thread::spawn.

Remarquez dans la Liste 16-1 que la closure que nous passons à thread::spawn ne prend aucun argument : nous n'utilisons aucune donnée du thread principal dans le code du thread lancé. Pour utiliser des données du thread principal dans le thread lancé, la closure du thread lancé doit capturer les valeurs dont elle a besoin. La Liste 16-3 montre une tentative de créer un vecteur dans le thread principal et de l'utiliser dans le thread lancé. Cependant, cela ne fonctionnera pas encore, comme vous le verrez tout de suite.

Nom de fichier : src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

Liste 16-3 : Tentative d'utiliser un vecteur créé par le thread principal dans un autre thread

La closure utilise v, donc elle capturera v et le fera faire partie de l'environnement de la closure. Comme thread::spawn exécute cette closure dans un nouveau thread, nous devrions être en mesure d'accéder à v à l'intérieur de ce nouveau thread. Mais lorsque nous compilons cet exemple, nous obtenons l'erreur suivante :

error[E0373]: closure may outlive the current function, but it borrows `v`,
which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {:?}", v);
  |                                           - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {:?}", v);
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced
variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

Rust déduit comment capturer v, et puisque println! a besoin seulement d'une référence à v, la closure essaie de prêter v. Cependant, il y a un problème : Rust ne peut pas savoir combien de temps le thread lancé va exécuter, donc il ne sait pas si la référence à v sera toujours valide.

La Liste 16-4 fournit un scénario où il est plus probable d'avoir une référence à v qui ne sera pas valide.

Nom de fichier : src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    drop(v); // oh no!

    handle.join().unwrap();
}

Liste 16-4 : Un thread avec une closure qui tente de capturer une référence à v d'un thread principal qui supprime v

Si Rust nous autorisait à exécuter ce code, il y aurait une possibilité que le thread lancé soit immédiatement mis en arrière-plan sans s'exécuter du tout. Le thread lancé a une référence à v à l'intérieur, mais le thread principal supprime immédiatement v, en utilisant la fonction drop dont nous avons parlé au chapitre 15. Ensuite, lorsque le thread lancé commence à s'exécuter, v n'est plus valide, donc une référence à elle n'est pas non plus valide. Oh non!

Pour corriger l'erreur du compilateur dans la Liste 16-3, nous pouvons suivre l'avis du message d'erreur :

help: to force the closure to take ownership of `v` (and any other referenced
variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

En ajoutant le mot-clé move avant la closure, nous forçons la closure à prendre la propriété des valeurs qu'elle utilise plutôt que de laisser Rust déduire qu'elle devrait prêter les valeurs. La modification de la Liste 16-3 montrée dans la Liste 16-5 compilera et s'exécutera comme nous le souhaitons.

Nom de fichier : src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

Liste 16-5 : Utilisation du mot-clé move pour forcer une closure à prendre la propriété des valeurs qu'elle utilise

Nous serions tentés d'essayer la même chose pour corriger le code de la Liste 16-4 où le thread principal appelait drop en utilisant une closure move. Cependant, ce remède ne fonctionnera pas car ce que la Liste 16-4 essaye de faire est interdit pour une raison différente. Si nous ajoutions move à la closure, nous déplacerions v dans l'environnement de la closure, et nous ne pourrions plus appeler drop dessus dans le thread principal. Nous obtiendrions cette erreur du compilateur à la place :

error[E0382]: use of moved value: `v`
  --> src/main.rs:10:10
   |
4  |     let v = vec![1, 2, 3];
   |         - move occurs because `v` has type `Vec<i32>`, which does not
implement the `Copy` trait
5  |
6  |     let handle = thread::spawn(move || {
   |                                ------- value moved into closure here
7  |         println!("Here's a vector: {:?}", v);
   |                                           - variable moved due to use in
closure
...
10 |     drop(v); // oh no!
   |          ^ value used here after move

Les règles d'appartenance de Rust nous ont sauvé encore! Nous avons eu une erreur avec le code de la Liste 16-3 parce que Rust était prudent et prêtait seulement v au thread, ce qui signifiait que le thread principal pourrait théoriquement invalider la référence du thread lancé. En disant à Rust de déplacer la propriété de v au thread lancé, nous garantissons à Rust que le thread principal ne va plus utiliser v. Si nous modifions la Liste 16-4 de la même manière, nous violons alors les règles d'appartenance lorsque nous essayons d'utiliser v dans le thread principal. Le mot-clé move remplace la valeur par défaut prudente de Rust qui consiste à prêter ; il ne nous permet pas de violer les règles d'appartenance.

Maintenant que nous avons couvert ce qu'est un thread et les méthodes fournies par l'API de thread, regardons quelques situations dans lesquelles nous pouvons utiliser des threads.

Sommaire

Félicitations ! Vous avez terminé le laboratoire Utiliser les threads pour exécuter du code simultanément. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.