Blog ENI : Toute la veille numérique !
-25€ dès 75€ sur les livres en ligne, vidéos... avec le code FUSEE25. J'en profite !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Rust
  3. Les threads et le multithreading en Rust
Extrait - Rust Développez des programmes robustes et sécurisés
Extraits du livre
Rust Développez des programmes robustes et sécurisés
4 avis
Revenir à la page d'achat du livre

Les threads et le multithreading en Rust

Introduction

Nous voilà enfin rendus à ce chapitre important dans l’étude du langage Rust. Tous les chapitres le sont, évidemment, mais celui-ci revêt une place particulière dans la mesure où, parmi les nombreux avantages du langage Rust, la sécurisation des threads, notamment dans leur utilisation concurrentielle, est un point fondamental.

Au même titre que la sécurisation de la gestion de la mémoire, grâce au rôle accru du compilateur, la sécurisation de l’utilisation des threads est un aspect qui fait du langage Rust un langage de programmation système particulièrement sûr.

En effet, un code multithread peut être particulièrement compliqué à maintenir, en particulier lorsque l’on essaie d’accéder depuis différents threads à une même ressource. Quel que soit le langage, ce champ de programmation est fréquemment sujet à de multiples problèmes à l’exécution, en général très compliqués à comprendre et à résoudre.

On peut citer par exemple les deux problèmes emblématiques suivants :

  • Plusieurs threads accèdent à une même ressource, mais dans un ordre aléatoire ou non défini. Autrement dit, on ne sait pas quel sera le premier thread à...

Premiers threads et parallélisme

1. Introduction

La façon triviale de créer un thread en langage Rust consiste en l’usage d’une fonction nommée spawn, disponible dans un module dédié aux threads, nommé thread.

La documentation de cette fonction est disponible à cette adresse : https://doc.rust-lang.org/std/thread/fn.spawn.html

La documentation du module est disponible à cette adresse : https://doc.rust-lang.org/std/thread/index.html

Ce module fournit les outils permettant de créer de nouveaux threads, quel que soit le système d’exploitation sur lequel se compile le programme. Il offre aussi un certain nombre d’outils de communication entre threads.

2. Usage de spawn

a. Introduction

On commence par créer un petit projet dédié à nos premiers exemples :

cargo new first_threads --bin 
>    Created binary (application) `first_threads` package 

Dans celui-ci, on déclare le module dont on a besoin :

use std::thread; 

Puis on crée un nouveau thread grâce à spawn qui, pour le moment, ne fera rien du tout :

use std::thread; 
 
fn main() {  
  
    thread::spawn(move || { 
        // On ne fait rien dans ce thread. 
    }); 
} 

Il n’y a rien de très spectaculaire dans ce code, d’autant qu’on ne peut pas vraiment vérifier ce qu’il se passe. Seule information intéressante : la fonction spawn utilise une closure en paramètre, tel qu’étudié dans le chapitre précédent.

Le traitement effectué par le nouveau thread est placé entre les accolades (dans ce premier exemple, on ne fait rien).

Prenons dès à présent un exemple concret, avec une tâche effectuée par le nouveau thread.

b. Premier exemple concret

Nous allons prendre en exemple une petite démonstration très classique :

  • On crée un nouveau thread, dans notre thread principal, qui va afficher un petit compteur de 1 à 20.

  • Le thread principal va lui aussi afficher un petit compteur de 1 à 10.

Dans les deux cas, chaque affichage du compteur est supposé survenir 10 millisecondes après...

Communication entre threads

1. Introduction

Un des problèmes récurrents du multithreading est la communication entre threads. Une première solution est de définir une ressource destinée aux échanges. Quand un thread accède à la ressource pour la lire ou la modifier, il place un verrou (lock) sur cette ressource de manière à éviter que plusieurs threads n’accèdent en même temps à la même ressource. Cette solution possède ses limites, quel que soit le langage, provoquant régulièrement des problèmes à l’exécution comme celui du deadlock.

Une autre solution consiste à établir un canal de communication entre deux threads. La terminologie en anglais de ce dispositif est channel. Cet intermédiaire privilégié entre deux threads est une solution proposée par le langage Rust ; elle est l’objet de la présente section.

2. Usage des canaux en Rust

a. Définition

Quand on parle d’un canal en développement multithreading Rust, on peut imaginer une sorte de tuyau dans lequel de l’information transite de manière unidirectionnelle ; seulement dans le sens A vers B. Si on a le thread A, il utilise donc le canal pour envoyer des informations au thread B via ce canal.

Pour les spécialistes d’Unix, cette description vous fera peut-être penser à la notion de « pipe ». La différence notable est qu’un pipe Unix transmet des octets (de A à B), alors que Rust utilise un canal pour transmettre des valeurs (de variables).

b. Première utilisation d’un canal

On commence par créer un projet dédié à l’usage des canaux en Rust :

cargo new channels --bin 
>     Created binary (application) `channels` package 

On a recours à un module de la librairie standard, dédié aux canaux : std::sync::mpsc. Sa documentation officielle en ligne se trouve ici : https://doc.rust-lang.org/std/sync/mpsc/

On commence par déclarer ce module afin de l’utiliser :

use std::sync::mpsc::channel; 

On aura également besoin de manipuler des threads, bien évidemment :

use std::thread; 

Enfin, et pour bien distinguer le thread qui envoie le message...

Usage des verrous mutuels exclusifs

1. Présentation et définition

Si on imagine un environnement massivement multithread avec une ressource partagée, a priori mutable, on va au-devant de gros problèmes de performances. En effet, comment faire si n threads veulent accéder régulièrement à une ressource mutable r ?

On sait que si on n’organise rien, on va forcément rencontrer des problèmes de concurrence, plusieurs threads tentant de lire la même ressource en même temps. Et cela paraît surtout et au premier abord en contradiction avec les principes de propriété de Rust.

On pourrait imaginer qu’un thread serait chargé de lire en permanence la ressource et enverrait régulièrement la valeur clonée aux threads intéressés grâce à des canaux. Cela paraît au premier abord fastidieux, mais ce pourrait être une première solution.

La bonne solution est en réalité le verrou mutex ou plutôt verrou mutuel exclusif. Les personnes ayant programmé des threads en langage C++ sont sans doute familiarisées avec cette notion. Pour les autres, la définition est assez simple :

« Un mutex est un dispositif qui oblige les threads intéressés par une ressource donnée à se placer dans une file d’attente. Le mutex gérant évidemment l’avancée de cette file, les threads qui y rentrent, les threads qui en sortent et comment, chacun à son tour, un thread accède à la ressource après avoir patienté. »

Il existe plusieurs avantages à utiliser un mutex, tous orientés vers la sécurité accrue dès la compilation. Ainsi, lors de l’exécution, les conflits d’accès à une même ressource (en lecture ou en écriture) par deux threads différents devraient être évités. Cela réduit également le caractère imprévisible de conflits entre threads lors de l’exécution.

Passons à présent à l’utilisation pratique de verrous mutuels exclusifs en langage Rust ; mais auparavant, nous allons dire un mot sur deux notions qui nous seront utiles : Rc et Arc.

2. Les structures...