Send & Sync

Send & Sync

■ **Rust : Send et Sync**

Guide Complet pour la Concurrence Thread-Safe

Comprendre les Marker Traits

Décembre 2025

1. Introduction aux Marker Traits

En Rust, Send et Sync sont des marker traits qui garantissent la sécurité de la concurrence à la compilation. Ils sont fondamentaux pour écrire du code concurrent sans data races.

Qu'est-ce qu'un Marker Trait ?

Un marker trait est un trait sans méthodes qui sert uniquement à marquer un type avec une propriété particulière. Send et Sync sont implémentés automatiquement par le compilateur pour la plupart des types.

// Définitions dans std::marker
pub unsafe auto trait Send { }
pub unsafe auto trait Sync { }
// auto trait = implémenté automatiquement
// unsafe = si implémenté manuellement, responsabilité du développeur

Définition

Send signifie qu'une valeur peut être transférée (moved) entre threads en toute sécurité. Si un type implémente Send, vous pouvez le déplacer d'un thread à un autre sans risque.

Exemple de base

use std::thread;
fn main() {
    let data = String::from("Hello");  // String est Send
    thread::spawn(move || {

//OK : String est Send, on peut le déplacer dans un autre thread println!("{}", data); });

}

Types Send courants

Exemple avec un type non-Send

use std::rc::Rc;
use std::thread;
fn main() {
    let data = Rc::new(String::from("Hello"));

//ERREUR : Rc<String> n'est pas Send ! thread::spawn(move || { println!("{}", data); }); }

// Erreur du compilateur :
// error[E0277]: `Rc<String>` cannot be sent between threads safely
//    = help: the trait `Send` is not implemented for `Rc<String>`

Définition

Sync signifie qu'une référence (&T;) peut être partagée entre threads en toute sécurité. Si T est Sync, alors &T; est Send.

`// Règle fondamentale`

T is Sync&T is Send

// Si T implémente Sync, alors une référence &T peut être envoyée entre threads

Exemple de base

use std::thread;
use std::sync::Arc;
fn main() {
    let data = Arc::new(String::from("Hello"));
    let data_ref = Arc::clone(&data);
    thread::spawn(move || {

//OK : String est Sync, donc &String est Send

        // Arc permet de partager la référence
        println!("{}", data_ref);
    });
    println!("{}", data);
}

Types Sync courants

4. Send vs Sync : Différences clés

Diagramme mental

5. Types courants et leurs propriétés

6. Cas pratiques : Axum et async

Dans Axum et Tokio, les traits Send et Sync sont cruciaux car les futures peuvent être déplacées entre threads.

Pourquoi Sync est nécessaire dans les traits

// Trait pour formulaires Runique

pub trait FormulaireTrait: Send + Sync { //Sync important !

    fn new() -> Self;
    fn validate(&mut self, raw_data: &HashMap<String, String>) -> bool;
}
// Sans Sync, cette erreur peut survenir :
#[async_trait]
impl<S, T> FromRequest<S> for AxumForm<T>
where

T: FormulaireTrait + 'static, //Doit être Send + Sync

{
    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        // Cette future peut être déplacée entre threads par Tokio
        // Si T n'est pas Sync et qu'une référence &T existe,
        // le compilateur rejettera le code
    }
}

Exemple concret avec Cell

use std::cell::Cell;

//Ce code ne compile PAS pub struct BadForm { inner: Forms, counter: Cell<u32>, // Cell n'est pas Sync ! }

impl FormulaireTrait for BadForm {
    fn new() -> Self {
        Self {
            inner: Forms::new(),
            counter: Cell::new(0),
        }
    }
    fn validate(&mut self, raw_data: &HashMap<String, String>) -> bool {
        self.counter.set(self.counter.get() + 1);
        self.inner.is_valid()
    }
}
// error[E0277]: `Cell<u32>` cannot be shared between threads safely
//    = help: the trait `Sync` is not implemented for `Cell<u32>`

Solution correcte

use std::sync::atomic::{AtomicU32, Ordering};

//Ce code compile ! pub struct GoodForm { inner: Forms, counter: AtomicU32, // AtomicU32 est Send + Sync } impl FormulaireTrait for GoodForm { fn new() -> Self { Self { inner: Forms::new(), counter: AtomicU32::new(0), } } fn validate(&mut self, raw_data: &HashMap<String, String>) -> bool { self.counter.fetch_add(1, Ordering::Relaxed); self.inner.is_valid() }

}

Erreur 1 : Rc dans un contexte async

//Erreur use std::rc::Rc;

async fn handler(data: Rc<String>) {
    // Erreur : Rc is not Send
}

//Solution use std::sync::Arc;

async fn handler(data: Arc<String>) {
    // OK : Arc is Send + Sync
}

Erreur 2 : Cell/RefCell dans un trait Sync

//Erreur use std::cell::Cell;

struct MyStruct {
    value: Cell<i32>,  // Cell n'est pas Sync
}

//Solution : Utiliser des types atomiques use std::sync::atomic::{AtomicI32, Ordering};

struct MyStruct {
    value: AtomicI32,  // AtomicI32 est Send + Sync
}

Erreur 3 : Oublier Sync dans un trait

//Risque futur pub trait MyTrait: Send { // Manque Sync // ... } //Meilleure pratique pub trait MyTrait: Send + Sync { // Complet et sûr // ...

}

Exemple 1 : Trait de formulaire correct

//Trait correct pour Axum pub trait FormulaireTrait: Send + Sync { fn new() -> Self; fn validate(&mut self, raw_data: &HashMap<String, String>) -> bool; }

// Implémentation
pub struct UserForm(Forms);
impl FormulaireTrait for UserForm {
    fn new() -> Self {
        Self(Forms::new())
    }
    fn validate(&mut self, raw_data: &HashMap<String, String>) -> bool {
        // Validation...
        self.0.is_valid()
    }
}
// Extracteur Axum
#[axum::async_trait]
impl<S, T> FromRequest<S> for AxumForm<T>
where
    S: Send + Sync,
    T: FormulaireTrait + 'static,  // T est automatiquement Send + Sync
{
    type Rejection = Response;
    async fn from_request(req: Request<Body>,state: &S)→
Result<Self,Self::Rejection> {
        // Le compilateur garantit que c'est thread-safe
        // ...
    }
}

Exemple 2 : État partagé dans Axum

use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Clone)]
struct AppState {
    counter: Arc<Mutex<i32>>,  // Mutex<T> est Sync si T: Send
    config: Arc<Settings>,      // Arc<T> est Sync si T: Sync
}

`async fn handler(`

    State(state): State<AppState>,
    let mut counter = state.counter.lock().await;

1. Toujours ajouter Send + Sync aux traits publics

Garantit la compatibilité avec async/await et Tokio.

2. Préférer Arc à Rc pour le code async

Arc est thread-safe, Rc ne l'est pas.

3. Utiliser AtomicXxx au lieu de Cell/RefCell

Pour la mutabilité intérieure thread-safe.

4. Documenter les contraintes Send/Sync

Facilite la compréhension pour les futurs développeurs.

5. Tester avec des références Arc

Vérifie que vos types sont bien Sync.

6. Comprendre les erreurs du compilateur

Les messages d'erreur Send/Sync sont très précis.

Exercice 1 : Identifier Send et Sync

Pour chaque type, déterminez s'il est Send et/ou Sync :

`// Ce code ne compile pas. Pourquoi ? Comment le corriger ?`

use std::rc::Rc;
use std::thread;

`fn main() {`

    let data = Rc::new(vec![1, 2, 3]);
    thread::spawn(move || {
        println!("{:?}", data);
    });
}

Exercice 3 : Implémenter un trait thread-safe

Créez un trait CacheTrait qui :

Solution Exercice 1

Solution Exercice 2

// Problème : Rc n'est pas Send
// Solution : Utiliser Arc au lieu de Rc

use std::sync::Arc; //Changement ici

use std::thread;

`fn main() {`

let data = Arc::new(vec![1, 2, 3]); //Arc au lieu de Rc

    thread::spawn(move || {
        println!("{:?}", data);
    });
}

//Compile et fonctionne !

Solution Exercice 3

use std::sync::{Arc, Mutex};
use std::collections::HashMap;
// Trait thread-safe pour cache
pub trait CacheTrait: Send + Sync {
    type Key: Send + Sync;
    type Value: Send + Sync;
    fn get(&self, key: &Self::Key) -> Option<Self::Value>;
    fn set(&self, key: Self::Key, value: Self::Value);
}
// Implémentation avec Mutex
pub struct Cache<K, V> {
    data: Arc<Mutex<HashMap<K, V>>>,
}
impl<K, V> Cache<K, V>
where
    K: Send + Sync + Eq + std::hash::Hash + Clone,
    V: Send + Sync + Clone,
{
    pub fn new() -> Self {
        Self {
            data: Arc::new(Mutex::new(HashMap::new())),
        }
    }
}
impl<K, V> CacheTrait for Cache<K, V>
where
    K: Send + Sync + Eq + std::hash::Hash + Clone,
    V: Send + Sync + Clone,
{
    type Key = K;
    type Value = V;
    fn get(&self, key: &Self::Key) -> Option<Self::Value> {
        self.data.lock().unwrap().get(key).cloned()
    }
    fn set(&self, key: Self::Key, value: Self::Value) {
        self.data.lock().unwrap().insert(key, value);
    }
}

//Ce cache est Send + Sync et utilisable dans du code async !

■ **Conclusion**

Send et Sync sont les piliers de la programmation concurrente sûre en Rust. Le compilateur les vérifie automatiquement, éliminant toute possibilité de data races.

■ Send = Peut être déplacé entre threads

■ Sync = Peut être partagé (référence) entre threads

■ Essentiel pour Axum, Tokio et async/await

■ **Ressources**

⚡ Runique exercise Beta
Say hello to start an exercise for this course.