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));  // 1

Arc<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.

TraitSignification
SendLe type peut être transféré (moved) entre threads
SyncLe 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

TypeSendSync
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 pointerUsageThread-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élismeApprocheComplexité
thread::spawnThreads OS brutsHaute
crossbeam::scopeThreads avec empruntMoyenne
mpsc / crossbeam_channelPassage de messagesFaible
rayonItérateurs parallèlesTrès faible
tokio / async-stdAsync / IO-boundHaute

🔗 Voir aussi