Send & Sync

Send & Sync

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éthode 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 par le compilateur pour tout type
// composé uniquement de champs Send/Sync
// unsafe = si implémenté manuellement, la responsabilité de la sécurité
// retombe sur le développeur

2. Le trait `Send`

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, on peut le déplacer d'un thread à un autre sans risque.

Exemple de base

use std::thread;

fn main() {
    let data = String::from("Hello");

    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ées possédées, pas de références partagées
Vec<T>✅ Oui (si T: Send)Idem, possède ses données
i32, u64, bool✅ OuiTypes primitifs copiables
Arc<T>✅ Oui (si T: Send + Sync)Pointeur atomique thread-safe
Rc<T>❌ NonCompteur de références non atomique
Cell<T>✅ Oui (si T: Send)Possède sa donnée, mais pas thread-safe en partage (voir Sync)

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);
    });
}
error[E0277]: `Rc<String>` cannot be sent between threads safely
   = help: the trait `Send` is not implemented for `Rc<String>`

3. Le trait `Sync`

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, 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 entre threads.
        println!("{}", data_ref);
    });

    println!("{}", data);
}

Types `Sync` courants

TypeSync ?Raison
String✅ OuiImmuable une fois construite, pas de mutabilité intérieure
Vec<T>✅ Oui (si T: Sync)Idem
i32, u64, bool✅ OuiTypes primitifs
Mutex<T>✅ Oui (si T: Send)Synchronisation explicite intégrée
Arc<T>✅ Oui (si T: Sync)Pointeur atomique
Rc<T>❌ NonCompteur de références non atomique
Cell<T>❌ NonMutabilité intérieure non thread-safe
RefCell<T>❌ NonVérifications d'emprunt à l'exécution, non thread-safe

4. `Send` vs `Sync` : différences clés

AspectSendSync
SignificationLa valeur peut être déplacée entre threadsUne référence vers la valeur peut être partagée entre threads
OwnershipTransfert de propriétéPartage de référence
Exemple d'usagemove dans thread::spawn&T accessible depuis plusieurs threads
Pattern typiquethread::spawn(move || data)Arc<T> partagé entre threads
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 un Arc<T> partagéT: Send + Sync
J'utilise un Mutex<T> partagéT: Send

Types courants et leurs propriétés (récapitulatif)

TypeSendSyncNotes
StringSûr pour la concurrence
Vec<T>✅*✅**si T: Send/Sync
HashMap<K,V>✅*✅**si K,V: Send/Sync
i32, u64, boolTypes primitifs
Arc<T>✅*✅**si T: Send + Sync
Rc<T>Compteur non atomique
Cell<T>✅**si T: Send — jamais Sync
RefCell<T>✅**si T: Send — vérif. d'emprunt runtime non thread-safe
Mutex<T>✅*✅**si T: Send (pas besoin de T: Sync)
RwLock<T>✅*✅**si T: Send + Sync

5. 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 par l'exécuteur (le runtime multi-thread de Tokio peut migrer une tâche d'un thread worker à un autre entre deux points d'.await).

Pourquoi `Sync` est nécessaire sur un trait de formulaire

// Trait générique illustratif pour un formulaire
pub trait FormulaireTrait: Send + Sync {   // ← Sync important !
    fn new() -> Self;
    fn validate(&mut self, raw_data: &HashMap<String, String>) -> bool;
}

// Sans Sync, une erreur de ce type peut survenir dans un extracteur Axum :
#[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 à travers un .await,
        // le compilateur rejette le code.
    }
}

Exemple concret : un type qui casse la compilation

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()
    }
}

6. Erreurs courantes et solutions

Erreur 1 — `Rc` dans un contexte async

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

async fn handler(data: Rc<String>) {
    // Erreur : Rc n'est pas Send
}
// Solution
use std::sync::Arc;

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

Erreur 2 — `Cell`/`RefCell` dans un type censé être `Sync`

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

struct MyStruct {
    value: Cell<i32>,   // Cell n'est pas Sync
}
// Solution : utiliser un type atomique
use std::sync::atomic::{AtomicI32, Ordering};

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

Erreur 3 — oublier `Sync` sur un trait public

// Risque futur : Send seul suffit aujourd'hui mais bloquera dès qu'un usage
// exigera Sync
pub trait MyTrait: Send {
    // ...
}

// Meilleure pratique : poser les deux bounds dès le départ si le trait
// est destiné à un contexte partagé/async
pub trait MyTrait: Send + Sync {
    // ...
}

7. Exemple illustratif — é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;
    // ...
}

8. Bonnes pratiques

  1. Ajouter Send + Sync aux traits publics destinés à un contexte async/partagé — garantit la compatibilité avec Tokio dès la conception, plutôt que de le découvrir à l'usage.
  2. Préférer Arc à Rc dans tout code touché par asyncArc est thread-safe, Rc ne l'est jamais.
  3. Utiliser AtomicXxx plutôt que Cell/RefCell dès qu'un partage entre threads est possible — mutabilité intérieure thread-safe.
  4. Documenter les contraintes Send/Sync explicitement dans les commentaires de trait — facilite la compréhension pour les futurs développeurs.
  5. Lire attentivement les erreurs du compilateur — les messages Send/Sync nomment précisément le type fautif et pourquoi.

9. Exercices

Exercice 1 — identifier `Send` et `Sync`

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

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

Exercice 2 — corriger le code

// 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éer un trait CacheTrait qui :

  • soit utilisable dans du code async ;
  • permette de stocker et récupérer des valeurs ;
  • soit thread-safe.

Solutions des exercices

Solution Exercice 1

TypeSendSyncExplication
StringType standard thread-safe
Vec<Rc<i32>>Rc n'est ni Send ni Sync, donc le Vec qui le contient non plus
Arc<Mutex<String>>Arc + Mutex = combinaison thread-safe standard
Cell<String>Send car String: Send, mais jamais Sync (mutabilité intérieure)
&strRéférence immuable vers des données Sync

Solution Exercice 2

// Problème : Rc n'est pas Send, impossible de le déplacer dans un thread.
// Solution : utiliser Arc à la place.
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 = la valeur peut être déplacée entre threads.
  • Sync = une référence vers la valeur peut être partagée entre threads.
  • Vérification entièrement faite à la compilation : élimine les data races par construction.
  • Auto-implémenté par le compilateur pour la quasi-totalité des types.
  • Essentiel dès qu'on touche Axum, Tokio, ou async/await en général.

Ressources

  • The Rust Book — chapitre 16 (Concurrency)
  • Rust Nomicon — Send and Sync
  • Documentation Tokio
  • Documentation Axum

Pour voir Send/Sync appliqués à du vrai code Runique (pas un exemple générique), voir ~/Bureau/revision/18_send_sync.md — extraits réels de runique/src/forms/base.rs et field.rs.

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