Шпаргалка по Send и Sync в Rust

Send и Sync — это marker traits (трейты-маркеры), которые управляют безопасностью многопоточности: Send разрешает перемещение значения между потоками, а Sync разрешает деление ссылки на значение между потоками (то есть &T должен быть Send). Эти трейты автоматически выводятся компилятором для всех типов, но могут быть явно указаны через unsafe impl, когда разработчик гарантирует, что тип не содержит data race — Send требует полного владения или атомарных операций, а Sync требует синхронизированного доступа к внутреннему состоянию.

Send — тип можно безопасно перемещать между потоками.

Sync — ссылку на тип можно безопасно делить между потоками.

Send  =  Sent to another thread  (перемещение ownership)
Sync  =  Shared across threads   (разделение ссылки)
ТипSendSyncПримечание
(), u8, i32, f64, bool, charПримитивы без внутреннего состояния
&T (неизменяемая ссылка)&T: Send только если T: Sync
&mut T (изменяемая ссылка)Владеет эксклюзивным доступом
Box<T>T: SendT: SyncВладение кучей
Vec<T>, String, HashMap<T, U>T: SendT: SyncВладение коллекцией
Option<T>, Result<T, E>T: Send, E: SendT: Sync, E: SyncКонтейнеры
fn() (function pointer)Без состояния
ClosureЕсли не захватывает !Sync
Arc<T>T: SendT: SyncАтомарный счётчик
Rc<T>Неатомарный счётчик
RefCell<T>T: SendВнутренняя мутабельность
Mutex<T>T: SendT: SyncСинхронизирует доступ
AtomicBool, AtomicUsize и т.д.Атомарные операции
UnsafeCell<T>T: SendЯдро внутренней мутабельности

Правила производных типов

  • Кортежи и структуры
(T1, T2, ..., Tn): Send  ⇔  T1: Send && T2: Send && ... && Tn: Send
(T1, T2, ..., Tn): Sync  ⇔  T1: Sync && T2: Sync && ... && Tn: Sync
  • Умные указатели
Box<T>: Send  ⇔  T: Send
Box<T>: Sync  ⇔  T: Sync

Rc<T>: Send  ⇔  ❌ (всегда false)
Rc<T>: Sync  ⇔  ❌ (всегда false)

Arc<T>: Send  ⇔  T: Send
Arc<T>: Sync  ⇔  T: Sync
  • Контейнеры
Vec<T>: Send  ⇔  T: Send
Vec<T>: Sync  ⇔  T: Sync + Clone  (для некоторых операций)

String: Send  ⇔  ✅ (всегда true)
String: Sync  ⇔  ✅ (всегда true)
  • Функции и замыкания
extern "C" fn(): Send  ⇔  ✅
extern "C" fn(): Sync  ⇔  ✅

FnMut closure: Send  ⇔  захваченные переменные: Send
FnOnce closure: Send ⇔  захваченные переменные: Send

Автотрейты (compiler-generated)

Rust автоматически реализует трейты Send/Sync. Не нужно писать имплементацию этих трейтов в большинстве случаев. Если программист все же вынужден это делать, то он ступает на дорогу Unsafe, где ответственность за безопасность данных ложится на него.

Sync и Send — автоматические и unsafe трейты-маркеры. Это значит, что:

  • У них нет методов и ассоциированных типов.
  • Их компилятор реализует для всех подходящих типов автоматически — не нужно писать impl и #[derive(..)].
  • Реализовать их самостоятельно — небезопасно. Можно, но тогда механизмы безопасности Rust не подстрахуют, потому что Send и Sync сами являются фундаментом этой безопасности.

Send и Sync реализуются компилятором автоматически, когда он видит, что тип обеспечивает такие гарантии. Если написать структуру, одно из полей которой не будет реализовывать Send и/или Sync, то и вся структура не будет реализовывать Send и/или Sync соответственно.

#[derive(Debug)]
struct MyStruct {
    field1: i32,    // Send ✅, Sync ✅
    field2: String, // Send ✅, Sync ✅
}

// Результат:
// MyStruct: Send ✅ (все поля Send)
// MyStruct: Sync ✅ (все поля Sync + Copy для field1)

Когда нужно unsafe impl

// Пример: структура с внешним ресурсом
struct MyFd {
    fd: RawFd,  // не-OWNING raw pointer
}

// Rust не может автоматически определить Send/Sync для raw pointers
// Поэтому нужно явно указать:

unsafe impl Send for MyFd {}   // если fd не используется в нескольких потоках
unsafe impl Sync for MyFd {}   // если доступ к fd синхронизирован внешними примитивами

Проверка в многопоточном коде:

use std::thread;

fn main() {
    let data = String::from("hello");
    thread::spawn(move || {
        println!("{}", data);
    }); // ✅ String: Send

    let data_rc = Rc::new(String::from("hello"));
    thread::spawn({
        let data_rc = data_rc.clone();
        move || {
            println!("{}", data_rc);
        }
    }); // ❌ Rc<String>: !Send
}

Типичные паттерны

Безопасные многопоточные структуры

use std::sync::{Arc, Mutex};

// Паттерн 1: Arc<Mutex<T>> - shared mutable state
let shared = Arc::new(Mutex::new(0));
let shared_clone = Arc::clone(&shared);
thread::spawn(move || {
    let mut num = shared_clone.lock().unwrap();
    *num += 1;
});

// Паттерн 2: Arc<Atomic*> - lock-free synchronization
use std::sync::atomic::{AtomicUsize, Ordering};
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
thread::spawn(move || {
    counter_clone.fetch_add(1, Ordering::SeqCst);
});

Небезопасные многопоточные структуры

use std::cell::RefCell;
use std::rc::Rc;

// Плохой пример:
let bad = Rc::new(RefCell::new(0));
thread::spawn({
    let bad_clone = bad.clone();
    move || {
        *bad_clone.borrow_mut() = 1; // ❌ RefCell: !Sync, Rc: !Send
    }
}); // compilation error!

Памятка: «Правило 3-х вопросов»

При определении Send/Sync для своего типа задайте:

1. Ownership может быть перемещён в другой поток?

    • Если нет → не Send
    • Если да → проверяйте Sync

    2. Ссылка &T может быть разделена между потоками?

    • Если нет → не Sync
    • Если да → &T: Send

    3. Есть ли внутреннее состояние, которое может вызвать data race?

    • Атомарные операции → безопасно
    • Неатомарные операции → требует синхронизации
    • Raw pointers → unsafe

    Частые причины ошибок

    • Rc<T> doesn’t implement Send — Неатомарный счётчик — Использовать Arc<T>
    • RefCell<T> doesn’t implement Sync — Внутренняя мутабельность без синхронизации — Использовать Mutex<T> или RwLock<T>
    • *mut <T> doesn’t implement Send — Raw pointer — Обернуть в безопасный тип с unsafe impl
    • Closure doesn’t implement Send — Захватывает !Send переменную — Использовать Arc<Mutex<T>> для захвата

    Ключевые типы для многопоточности

    // Для shared mutable state (один writer или many readers):
    Arc<Mutex<T>>       // блокировки
    Arc<RwLock<T>>      // множественные читатели, один writer
    
    // Для lock-free atomic operations:
    Arc<AtomicUsize>    // атомарные операции без блокировок
    Arc<AtomicBool>
    AtomicPtr<T>
    
    // Для ownership в одном потоке:
    Rc<T>               // не для многопоточности!
    RefCell<T>          // не для многопоточности!
    Cell<T>             // не для многопоточности!

    Запомните: Send и Sync — это compiler-enforced guarantees о безопасности многопоточности. Если код компилируется с thread::spawn, то он гарантированно free от data race! 🎉

    Дополнительные ресурсы

    Rust Reference: Send trait
    Rust Reference: Sync trait
    Rustonomicon: Unsafe Code Guidelines
    Rust Reference: Module Atomic
    Rust By Example: Shared State

    От admin

    StackUP