RustMiddleTechnical
Что такое Send и Sync и почему они важны для конкурентного программирования?
Send означает, что тип можно переместить в другой поток; Sync — что на него можно иметь разделяемую ссылку из нескольких потоков. Компилятор выводит эти трейты автоматически и использует их для статической гарантии отсутствия data races.
Send и Sync: маркерные трейты безопасного параллелизма
Rust гарантирует отсутствие гонок данных (data races) на уровне системы типов с помощью двух маркерных трейтов из std::marker:
- Send — тип можно безопасно передать (переместить) в другой поток.
- Sync — на тип можно иметь разделяемую ссылку из нескольких потоков одновременно. Иначе говоря,
T: Syncтогда и только тогда, когда&T: Send.
Оба трейта реализуются автоматически (auto traits) компилятором для большинства типов. Вы не пишете impl Send for MyType вручную в обычном коде — компилятор выводит это на основе полей структуры.
Примеры типов и их Send/Sync статус
i32, String, Vec<T>— Send + Sync (если T: Send + Sync).Rc<T>— !Send, !Sync: счётчик ссылок не атомарный, совместный доступ из нескольких потоков приведёт к гонке.Arc<T>— Send + Sync (если T: Send + Sync): атомарный счётчик ссылок.RefCell<T>— Send (если T: Send), но !Sync: внутренняя мутабельность без синхронизации.Mutex<T>— Send + Sync (если T: Send): мьютекс защищает доступ.- Сырые указатели
*const T,*mut T— !Send, !Sync.
Практический пример: Arc + Mutex для shared state
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u64));
let mut handles = vec![];
for _ in 0..8 {
let c = Arc::clone(&counter);
let h = thread::spawn(move || {
// move захватывает c (Arc<Mutex<u64>>)
// Arc: Sync => &Arc можно передать; Arc: Send => можно переместить
let mut guard = c.lock().unwrap();
*guard += 1;
});
handles.push(h);
}
for h in handles {
h.join().unwrap();
}
println!("Counter: {}", *counter.lock().unwrap());
}
Почему Rc нельзя передать между потоками
use std::rc::Rc;
use std::thread;
fn main() {
let val = Rc::new(42);
// Ошибка компиляции: `Rc<i32>` cannot be sent between threads safely
// thread::spawn(move || println!("{}", val));
}
Ручная реализация Send/Sync (unsafe)
Если ваш тип содержит сырой указатель, но вы знаете, что он безопасен для передачи между потоками, можно вручную объявить Send/Sync через unsafe impl:
struct MyWrapper(*mut u8);
// SAFETY: мы гарантируем, что *mut u8 не используется конкурентно без синхронизации
unsafe impl Send for MyWrapper {}
unsafe impl Sync for MyWrapper {}
Подводные камни
- !Send тип в tokio::spawn:
tokio::spawnтребуетFuture: Send + 'static. Случайный захватRcилиRefCellв async-блоке вызовет ошибку компиляции с длинным диагностическим сообщением. - Ручной unsafe impl без инвариантов: объявить
unsafe impl Sendпросто — нарушить гарантии ещё проще. Это единственный способ получить data race в безопасном Rust-коде. - Sync != Copy: многие путают эти трейты. Sync означает безопасность разделяемого доступа, а не возможность копирования.
- PhantomData: если вы добавляете
PhantomData<*mut T>, структура автоматически становится !Send + !Sync. Это правильное поведение для FFI-типов, но может удивить. - Взаимодействие с interior mutability:
Cell<T>— !Sync, даже если T: Sync, потому что позволяет изменять значение через shared reference без синхронизации. - Arc<RefCell<T>> не Sync: комбинация Arc (Sync) и RefCell (!Sync) даёт !Sync для всей обёртки — это частая ошибка при попытке shared mutable state.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.