6. Smart Pointers et Concurrence
Les smart pointers (Box, Rc, Arc, RefCell) ajoutent des capacités au-delà des références classiques. Combinés aux garanties de concurrence de Rust (Send, Sync), ils permettent de construire des systèmes thread-safe sans garbage collector.
6.1 Smart Pointers
Box<T> — Allocation sur le heap
Box place une valeur sur le heap plutôt que sur la stack. Utile pour :
- Les types de taille inconnue à la compilation (ex. récursifs)
- Le transfert de grandes structures sans copie
#[derive(Debug)]
enum GradientNode {
Value(f64),
Children(Vec<Box<GradientNode>>), // taille connue : un pointeur
}
fn main() {
let tree = GradientNode::Children(vec![
Box::new(GradientNode::Value(1.0)),
Box::new(GradientNode::Value(2.0)),
]);
// Box comme newtype pour heap-allouer une grosse structure
let big_gradients = Box::new([0.0; 1_000_000]);
// big_gradients est sur le heap, Box<[f64; 1M]> ne prend que 8 octets sur la stack
}Rc<T> — Reference Counting (mono-thread)
Permet le partage ownership (plusieurs propriétaires) dans un même thread. Chaque clone() incrémente un compteur.
use std::rc::Rc;
let g1 = Rc::new(vec![1.0, 2.0, 3.0]);
let g2 = Rc::clone(&g1); // référence partagée, pas de copie des données
println!("ref count: {}", Rc::strong_count(&g1)); // 2
drop(g2);
println!("ref count: {}", Rc::strong_count(&g1)); // 1Arc<T> — Atomic Reference Counting (multi-thread)
Version thread-safe de Rc. Utilise des opérations atomiques (légèrement plus lent, mais Send + Sync).
use std::sync::Arc;
use std::thread;
let gradients = Arc::new(vec![1.0, 2.0, 3.0]);
let mut handles = vec![];
for i in 0..3 {
let g = Arc::clone(&gradients); // incrémentation atomique
handles.push(thread::spawn(move || {
println!("worker {i}: gradient[0] = {}", g[0]);
}));
}
for h in handles {
h.join().unwrap();
}RefCell<T> — Mutabilité intérieure (mono-thread)
Permet de modifier une valeur même si elle est derrière une référence &T. Les règles de borrowing sont vérifiées à l’exécution.
use std::cell::RefCell;
// Stocker un gradient accumulé
let accumulated = RefCell::new(vec![0.0; 5]);
fn add_gradient(g: &[f64], acc: &RefCell<Vec<f64>>) {
let mut acc_borrow = acc.borrow_mut(); // runtime borrow check
for (a, &b) in acc_borrow.iter_mut().zip(g) {
*a += b;
}
} // release ici
add_gradient(&[1.0, 2.0, 3.0, 4.0, 5.0], &accumulated);
println!("{:?}", accumulated.borrow());⚠️ RefCell panique si on viole les règles d’emprunt à l’exécution (ex. borrow_mut deux fois).
Mutex<T> — Mutabilité intérieure thread-safe
Comme RefCell mais pour plusieurs threads :
use std::sync::Mutex;
let shared = Arc::new(Mutex::new(vec![0.0; 5]));
let mut handles = vec![];
for _ in 0..4 {
let shared = Arc::clone(&shared);
handles.push(thread::spawn(move || {
let mut data = shared.lock().unwrap();
for d in data.iter_mut() {
*d += 1.0;
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("{:?}", *shared.lock().unwrap()); // [4.0, 4.0, 4.0, 4.0, 4.0]6.2 Send et Sync
Ce sont des traits marqueurs (sans méthode). Le compilateur les implémente automatiquement quand le type est thread-safe.
| Trait | Signification |
|---|---|
Send | Le type peut être transféré (moved) entre threads |
Sync | Le type peut être partagé (référencé) entre threads (&T est Send) |
// Rc n'est pas Send (pas atomique)
fn main() {
let rc = Rc::new(42);
std::thread::spawn(move || {
println!("{rc}"); // ⛔ ERREUR : Rc<i32> n'est pas Send
});
}
// Arc est Send + Sync
fn main() {
let arc = Arc::new(42);
std::thread::spawn(move || {
println!("{arc}"); // ✅
});
}Tableau des types courants
| Type | Send | Sync |
|---|---|---|
Box<T> | ✅ si T: Send | ✅ si T: Sync |
Rc<T> | ❌ | ❌ |
Arc<T> | ✅ si T: Send + Sync | ✅ si T: Send + Sync |
RefCell<T> | ✅ | ❌ |
Mutex<T> | ✅ | ✅ |
Cell<T> | ✅ | ❌ |
AtomicBool / AtomicUsize | ✅ | ✅ |
Règle empirique : si votre type ne contient que des types Send + Sync, il est automatiquement Send + Sync. Le compilateur vous le dira si ce n’est pas le cas.
6.3 Concurrence avec thread::spawn
Création de threads
use std::thread;
fn aggregate_parallel(gradients: &[Vec<f64>], n_threads: usize) -> Vec<f64> {
let d = gradients[0].len();
let result = Arc::new(Mutex::new(vec![0.0; d]));
let chunk_size = gradients.len() / n_threads;
crossbeam::scope(|scope| {
for chunk in gradients.chunks(chunk_size) {
let result = Arc::clone(&result);
scope.spawn(move || {
for g in chunk {
let mut res = result.lock().unwrap();
for (r, &val) in res.iter_mut().zip(g) {
*r += val;
}
}
});
}
}).unwrap();
let result = Arc::try_unwrap(result).unwrap().into_inner().unwrap();
result.iter().map(|&x| x / gradients.len() as f64).collect()
}crossbeam — Thread scopes
Les scoped threads permettent d’emprunter des références sans Arc :
fn aggregate_scoped(gradients: &[Vec<f64>], n_threads: usize) -> Vec<f64> {
let d = gradients[0].len();
let result = Mutex::new(vec![0.0; d]);
crossbeam::scope(|scope| {
for chunk in gradients.chunks(gradients.len() / n_threads) {
scope.spawn(|| {
let mut res = result.lock().unwrap();
for g in chunk {
for (r, &val) in res.iter_mut().zip(g) {
*r += val;
}
}
});
}
}).unwrap();
result.into_inner().unwrap()
.iter().map(|&x| x / gradients.len() as f64).collect()
}6.4 Send + Sync et PyO3
Quand on prépare une librairie Rust appelable depuis Python via PyO3, les fonctions exposées doivent être Send.
use pyo3::prelude::*;
// Une GAR doit être Send pour être appelée depuis plusieurs threads Python
#[pyclass]
struct Krum {
f: usize,
}
#[pymethods]
impl Krum {
#[new]
fn new(f: usize) -> Self {
Self { f }
}
fn aggregate(&self, py: Python, grads: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
py.allow_threads(move || {
// GIL libéré : plusieurs appels peuvent s'exécuter en parallèle
self.do_aggregate(&grads)
})
}
}
// Vérification que Krum est bien Send + Sync
fn assert_send<T: Send>() {}
fn _check() { assert_send::<Krum>(); }6.5 Channels — Communication entre threads
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
let workers: Vec<_> = (0..4).map(|i| {
let tx = tx.clone();
thread::spawn(move || {
let gradient = vec![i as f64; 100];
tx.send((i, gradient)).unwrap();
})
}).collect();
drop(tx); // ferme le canal côté envoi
for (worker_id, grad) in rx {
println!("reçu gradient du worker {worker_id}");
}6.6 Rayon — Parallélisme de données simplifié
Rayon transforme les itérateurs en itérateurs parallèles :
use rayon::prelude::*;
fn aggregate_parallel_rayon(gradients: &[Vec<f64>]) -> Vec<f64> {
let n = gradients.len() as f64;
let d = gradients[0].len();
// Parallélisation automatique
let sums: Vec<f64> = (0..d)
.into_par_iter() // itération parallèle
.map(|j| {
gradients.iter()
.map(|g| g[j])
.sum::<f64>()
})
.collect();
sums.into_iter().map(|s| s / n).collect()
}6.7 Résumé
| Smart pointer | Usage | Thread-safe |
|---|---|---|
Box<T> | Heap allocation | ✅ (si T: Send) |
Rc<T> | Partage ownership (1 thread) | ❌ |
Arc<T> | Partage ownership (multi-thread) | ✅ |
RefCell<T> | Mutabilité intérieure (1 thread) | ❌ |
Mutex<T> | Mutabilité intérieure (multi-thread) | ✅ |
Cell<T> | Mutabilité par copie (types Copy) | ❌ |
| Parallélisme | Approche | Complexité |
|---|---|---|
thread::spawn | Threads OS bruts | Haute |
crossbeam::scope | Threads avec emprunt | Moyenne |
mpsc / crossbeam_channel | Passage de messages | Faible |
rayon | Itérateurs parallèles | Très faible |
tokio / async-std | Async / IO-bound | Haute |