English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Rust 并发编程

Le traitement sécurisé et efficace de la concurrence est l'un des objectifs de la naissance de Rust, principalement pour résoudre la capacité de charge élevée des serveurs.

Le concept de concurrence (concurrent) est que différentes parties du programme s'exécutent indépendamment, ce qui peut prêter à confusion avec le concept de parallélisme (parallel), qui met l'accent sur l'exécution "simultanée".

La concurrence peut souvent entraîner la parallélisation.

Ce chapitre traite des concepts et des détails de programmation liés à la concurrence.

Thread

Un thread (thread) est une partie indépendante d'exécution d'un programme.

Ce qui différencie les threads des processus (process) est que les threads sont un concept à l'intérieur du programme, et le programme est généralement exécuté dans un processus.

Dans un environnement avec système d'exploitation, les processus sont généralement exécutés par alternance de planification, tandis que les threads sont planifiés à l'intérieur du processus par le programme.

En raison de la concurrence possible des threads, les verrous mortels et les erreurs de délai qui peuvent survenir en parallèle sont souvent observés dans les programmes contenant des mécanismes de concurrence.

Pour résoudre ces problèmes, de nombreux autres langages (comme Java, C#) utilisent des logiciels spéciaux de runtime pour coordonner les ressources, mais cela réduit incontestablement l'efficacité d'exécution des programmes.

C/C++ Le langage prend en charge le multithreading au niveau le plus bas du système d'exploitation, et le langage lui-même ainsi que son compilateur ne disposent pas de la capacité de surveillance et d'éviter les erreurs parallèles, ce qui représente une grande pression pour les développeurs, qui doivent dépenser beaucoup d'énergie pour éviter les erreurs.

Rust ne dépend pas de l'environnement d'exécution, ce qui est similaire à C/C++ pareil.

Mais Rust a été conçu dans le langage lui-même pour inclure des moyens tels que le mécanisme d'ownership pour éliminer au maximum les erreurs les plus courantes à l'étape de compilation, ce que d'autres langages ne possèdent pas.

Mais cela ne signifie pas que nous pouvons ne pas être attentifs à la programmation, jusqu'à présent, les problèmes causés par la concurrence n'ont pas été complètement résolus au niveau public, et il pourrait y avoir des erreurs. Soyez prudents lors de la programmation en concurrence !

Dans Rust, un nouveau processus est créé par la fonction std::thread::spawn :

use std::thread;
use std::time::Duration;
fn spawn_function() {
    for i in 0..5 {
        println!("thread a été démarré et imprime {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}
fn main() {
    thread::spawn(spawn_function);
    for i in 0..3 {
        println!("thread principal imprime {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

Résultat de l'exécution :

thread principal imprime 0
thread a été démarré et imprime 0
thread principal imprime 1
thread a été démarré et imprime 1
thread principal imprime 2
thread a été démarré et imprime 2

Ce résultat pourrait changer d'ordre dans certains cas, mais dans l'ensemble, c'est ainsi qu'il est imprimé.

Ce programme a un sous-thread, l'objectif est d'imprimer 5 Les lignes de texte, le thread principal imprime trois lignes de texte, mais il est évident que avec la fin du thread principal, le thread spawné s'est également terminé et n'a pas terminé toutes les impressions.

Le paramètre de la fonction std::thread::spawn est une fonction sans paramètres, mais l'écriture ci-dessus n'est pas recommandée. Nous pouvons utiliser des closures (closures) pour transmettre des fonctions en tant que paramètres :

use std::thread;
use std::time::Duration;
fn main() {
    thread::spawn(|| {
        for i in 0..5 {
            println!("thread a été démarré et imprime {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 0..3 {
        println!("thread principal imprime {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

Les closures peuvent être stockées dans des variables ou transmises en tant que paramètres à d'autres fonctions. Une closure est équivalente à une expression Lambda dans Rust, et son format est le suivant :

|paramètre1, paramètre2, ...| -) > type de retour {
    // corps de la fonction
}

par exemple :

fn main() {
    let inc = |num: i32| -) > i32 {
        num + 1
    });
    println!("inc(5) = {}", inc(5));
}

Résultat de l'exécution :

inc(5) = 6

Les closures peuvent omettre la déclaration de type et utiliser le mécanisme de détection de type automatique de Rust :

fn main() {
    let inc = |num| {
        num + 1
    });
    println!("inc(5) = {}", inc(5));
}

le résultat ne change pas.

méthode join

use std::thread;
use std::time::Duration;
fn main() {
    let handle = thread::spawn(|| {
        for i in 0..5 {
            println!("thread a été démarré et imprime {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 0..3 {
        println!("thread principal imprime {}", i);
        thread::sleep(Duration::from_millis(1));
    }
    handle.join().unwrap();
}

Résultat de l'exécution :

thread principal imprime 0 
thread a été démarré et imprime 0 
thread a été démarré et imprime 1 
thread principal imprime 1 
thread a été démarré et imprime 2 
thread principal imprime 2 
thread a été démarré et imprime 3 
thread a été démarré et imprime 4

La méthode join permet de stopper le programme après que le sous-thread ait terminé son exécution.

Migration de propriété forcée par move

C'est une situation fréquente :

use std::thread;
fn main() {
    let s = "hello";
    
    let handle = thread::spawn(|| {
        println!("{}", s);
    });
    handle.join().unwrap();
}

Essayer d'utiliser les ressources du fonction courant dans un sous-thread est certainement une erreur ! Car le mécanisme de propriété interdit cette situation dangereuse, ce qui pourrait nuire à la certitude de destruction des ressources par le mécanisme de propriété. Nous pouvons utiliser le mot-clé move des closures pour traiter :

use std::thread;
fn main() {
    let s = "hello";
    
    let handle = thread::spawn(move || {
        println!("{}", s);
    });
    handle.join().unwrap();
}

Transmission de messages

Dans Rust, l'un des outils principaux pour la transmission de messages et la programmation concurrente est le canal (channel), qui se compose de deux parties : un émetteur (transmitter) et un récepteur (receiver).

std::sync::mpsc contient des méthodes de transmission de messages :

use std::thread;
use std::sync::mpsc;
fn main() {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
    let received = rx.recv().unwrap();
    println!("Obtenu: {}", received);
}

Résultat de l'exécution :

Obtenu: hi

Le sous-thread a obtenu le sendeur du thread principal tx, et a appelé sa méthode send pour envoyer une chaîne de caractères, puis le thread principal a reçu cette chaîne via le récepteur correspondant rx.