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

TypeSend ?Raison
String■OuiDonnéespossédées,pas de référencespartagées
Vec■Oui (si T: Send)Idem,possède ses données
i32, u64, bool■OuiTypesprimitifs copiables
Arc■Oui (si T: Send + Sync)Pointeur atomique thread-safe
Rc■NonCompteur de références non atomique
Cell■NonMutabilité intérieure non thread-safe

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

TypeSync ?Raison
String■OuiImmuable, pas de mutabilité intérieure
Vec■Oui (si T: Sync)Idem
i32, u64, bool■OuiTypes primitifs
Mutex■Oui (si T: Send)Synchronisation explicite
Arc■Oui (si T: Sync)Pointeur atomique
Rc■NonCompteur non atomique
Cell■NonMutabilité intérieure non atomique
RefCell■NonVérifications à l'exécution non thread-safe

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

AspectSendSync
SignificationJe peux être déplacé entre threadsMa référence peut être partagée entre threads
OwnershipTransfert de propriétéPartage de référence
Exemple usagemove dans thread::spawn&T accessible depuis plusieurs threads
Pattern typiquethread::spawn(move
VérificationÀ la compilationÀ la compilation

Diagramme mental

SituationTrait requis
Je déplace une valeur dans un autre threadSend
Je partage une référence entre threadsSync
J'utilise Arc partagéT: Send + Sync
J'utilise Mutex partagéT: Send

5. Types courants et leurs propriétés

TypeSendSyncNotes
StringSûr pour concurrence
Vec■*■** si T: Send/Sync
HashMap<K,V>■*■** si K,V: Send/Sync
i32, u64, boolTypes primitifs
Arc■*■** si T: Send+Sync / T:Sync
RcCompteur non atomique
Cell■** si T: Send, mais pas Sync
RefCell■*Borrow checking runtime
Mutex■*■** si T: Send
RwLock■*■** si T: Send+Sync

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()
    }
}
  • // Erreur du compilateur :
// 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>,
  • ) -> Response {
  • //OK : AppState est Send + Sync
    let mut counter = state.counter.lock().await;
  • *counter += 1;
  • // ...
  • }

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 :

TypeSend ?Sync ?
String??
Vec<Rc>??
Arc<Mutex>??
Cell??
&str??

`// 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 :

  • Soit utilisable dans du code async
  • Permette de stocker et récupérer des valeurs
  • Soit thread-safe

Solution Exercice 1

TypeSendSyncExplication
StringType standard thread-safe
Vec<Rc>Rc n'est ni Send ni Sync
Arc<Mutex>Arc + Mutex = thread-safe
CellSend car String:Send, mais pas Sync
&strRéférence immuable

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

  • Vérification à la compilation = Pas de data races
  • Auto-implémenté pour la plupart des types

■ Essentiel pour Axum, Tokio et async/await

■ **Ressources**

  • The Rust Book - Chapter 16 (Concurrency)
  • Rust Nomicon - Send and Sync
  • Tokio documentation
  • Axum documentation