# Параллелизм на уровне типов: Всё о маркерных типажах Send и Sync в Rust

> Подробный разбор маркерных типажей Send и Sync в Rust: соотношение T: Sync <=> &T: Send, почему Rc не поток-безопасен, разница между Send и Sync для RefCell, и требования thread::spawn.

- **URL:** https://eagle2000.space/rust/rust-send-sync-traits/
- **Date:** 2026-08-11

- **Tags:** rust, dev, send, sync, multithreading, concurrency, rc, refcell

---
## Введение

Главное обещание Rust в области многопоточности звучит амбициозно: **«Fearless Concurrency» (Бесстрашный параллелизм)**. Компилятор `rustc` гарантирует полное отсутствие гонок данных (**Data Races**) на этапе компилляции — программа с потенциальной гонкой данных просто не скомпилируется.

Но как компилятор узнает, какие структуры данных можно передавать между потоками, а какие — категорически нельзя?

Секрет кроется в двух встроенных маркерных типажах (**Auto Traits**): **`Send`** и **`Sync`**.

---

## 1. Философия `Send` и `Sync`

В Rust безопасность многопоточного доступа выражается прямо в системе типов:

1. **`Send`**: показывает, что **владение** типом `T` можно безопасно передать из одного потока в другой.
2. **`Sync`**: показывает, что **иммутабельные ссылки `&T`** можно безопасно разделять между несколькими потоками одновременно.

Между этими маркерными типажами существует строгое фундаментальное правило:

`T: Sync` тогда и только тогда, когда `&T: Send`

Это означает: если тип `T` является `Sync`, вы можете безопасно создать ссылку `&T` и передать её (`Send`) в другой поток для одновременного чтения.

### Авто-типажи (Auto Traits)

Типажи `Send` и `Sync` выводятся компилятором автоматически (**Auto Traits**). Если все поля вашей структуры реализуют `Send` и `Sync`, ваша структура тоже автоматически становится `Send` и `Sync`.

Попробуйте запустить код и проверить поведение `Send` и `Sync` в интерактивном блоке ниже:


### 💻 Маркерные типажи Send и Sync


#### Шаг 1

```rust
use std::thread;

// Шаг 1: Автоматические маркерные типажи Send и Sync
struct SafeData {
    value: String,
}

// Структура SafeData автоматически реализует Send и Sync,
// так как поле String реализует Send и Sync.

fn main() {
    let data = SafeData { value: String::from("Привет из главного потока!") };

    // Ключевое слово move передаёт владение `data` внутрь замыкания потока.
    // Функция thread::spawn требует, чтобы замыкание и его данные реализовывали Send + 'static.
    let handle = thread::spawn(move || {
        println!("Данные внутри фонового потока: {}", data.value);
    });

    handle.join().unwrap();
}
```

**1. Философия Send и Sync**
- `Send` означает, что владение объектом типа `T` можно безопасно передать в другой поток.
- `Sync` означает, что иммутабельные ссылки `&T` можно безопасно разделять между несколькими потоками одновременно (`T: Sync` ⟺ `&T: Send`).
- Оба типажа являются маркерными (Auto Traits) и вычитаются компилятором автоматически.


#### Шаг 2

```rust
use std::thread;
// Шаг 2: Ручная реализация Send / Sync для пользовательских оберток над сырыми указателями
struct MyRawWrapper<T> {
    ptr: *mut T,
}

// Сырые указатели (*mut T) по умолчанию НЕ реализуют Send и Sync.
// Если мы гарантируем потокобезопасность инвариантов вручную, мы используем unsafe impl:
unsafe impl<T: Send> Send for MyRawWrapper<T> {}
unsafe impl<T: Sync> Sync for MyRawWrapper<T> {}

fn main() {
    let mut val = 100i32;
    let wrapper = MyRawWrapper { ptr: &mut val as *mut i32 };

    let handle = thread::spawn(move || {
        println!("Сырой указатель успешно передан в фоновый поток через unsafe impl Send");
    });

    handle.join().unwrap();
}
```

**2. Ручная реализация unsafe impl Send / Sync**
- Сырые указатели `*const T` и `*mut T` считаются `!Send` и `!Sync`, так как компилятор не может проверить правила заимствований.
- Обёртки над ними требуют явной инструкции `unsafe impl Send` только после тщательного аудита потокобезопасности.


---

## 2. Замыкания и требования `thread::spawn`

Когда вы создаете новый поток через `std::thread::spawn`, функция предъявляет жесткие требования к передаваемому замыканию:

```rust
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,
```

Замыкание `F` должно быть:
* **`Send`**: чтобы передать захваченный контекст в создаваемый поток.
* **`'static`**: чтобы гарантировать, что создаваемый поток не переживет ссылки на данные, оставшиеся в родительском потоке.

> [!TIP]
> Для передачи владеющих данных в фоновый поток всегда используйте ключевое слово **`move`** перед замыканием: `thread::spawn(move || { ... })`.

---

## 3. Почему `Rc` и `RefCell` не являются поток-безопасными?

Это один из самых частых вопросов на собеседованиях по Rust.

### 1. `Rc<T>` — не `Send` и не `Sync`
Умный указатель `Rc<T>` (Reference Counting) увеличивает и уменьшает счетчик сильных и слабых ссылок простыми неатомарными операциями (`count += 1`). Если бы `Rc` передавался в другой поток, вызов `clone()` в одном потоке и `drop()` в другом привели бы к неконтролируемой перезаписи памяти и **Data Race**.

Для многопоточных сценариев используется **`Arc<T>`** (Atomic Reference Counting).

### 2. `RefCell<T>` — `Send`, но НЕ `Sync`
`RefCell<T>` позволяет изменять данные по иммутабельной ссылке через runtime-счетчик `borrow()` / `borrow_mut()`.
* Вы можете **передать сам `RefCell`** по значению в другой поток (`Send`), так как владение полностью меняет владельца.
* Но вы **НЕ можете передать ссылку `&RefCell`** в несколько потоков одновременно (`!Sync`), потому что флаг активных заимствований не защищен атомиками или мьютексом.

Для многопоточной внутренней мутабельности используют **`Arc<Mutex<T>>`** или **`Arc<RwLock<T>>`**.

Ниже представлен пошаговый пример работы `Arc` и `Mutex`:


### 💻 Потокобезопасность Rc, RefCell, Arc и Mutex


#### Шаг 1

```rust
use std::sync::Arc;
use std::thread;

// Шаг 1: Почему Rc не является Send, а Arc — является
fn main() {
    // Arc<T> (Atomic Reference Counting) использует атомарные операции для счетчика ссылок,
    // поэтому Arc<T> реализует Send + Sync (при T: Send + Sync) и может передаваться между потоками!
    let shared_data = Arc::new(vec![1, 2, 3]);
    let data_clone = Arc::clone(&shared_data);

    let handle = thread::spawn(move || {
        println!("Атомарный Arc в фоновом потоке: {:?}", data_clone);
    });

    handle.join().unwrap();
    println!("Главный поток удерживает Arc, сильные ссылки: {}", Arc::strong_count(&shared_data));
}
```

**1. Почему Rc не реализует Send**
- `Rc<T>` изменяет счетчик ссылок через обычное неатомарное инкрементирование (`count += 1`).
- Если бы `Rc<T>` передавался в другой поток, одновременный `clone` или `drop` привел бы к состояниям гонки (Data Race) и порче памяти.
- Для многопоточного разделяемого владения строго используется `Arc<T>`.


#### Шаг 2

```rust
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
use std::thread;

// Шаг 2: Почему RefCell является Send, но НЕ является Sync
fn main() {
    // 1. RefCell<T> реализует Send (его можно полностью переместить в другой поток)
    let cell = RefCell::new(42);
    let handle = thread::spawn(move || {
        *cell.borrow_mut() += 10;
        println!("RefCell перемещен и изменен в другом потоке: {}", cell.borrow());
    });
    handle.join().unwrap();

    // 2. Для одновременного доступа к данным из НЕСКОЛЬКИХ потоков по &T используется Arc<Mutex<T>>
    let mutex_data = Arc::new(Mutex::new(vec!["данные"]));
    let mutex_clone = Arc::clone(&mutex_data);

    let thread_handle = thread::spawn(move || {
        let mut lock = mutex_clone.lock().unwrap();
        lock.push("из фонового потока");
    });

    thread_handle.join().unwrap();
    println!("Результирующий вектор под Mutex: {:?}", mutex_data.lock().unwrap());
}
```

**2. Почему RefCell реализует Send, но НЕ реализует Sync**
- `RefCell<T>` проверяет динамические заимствования (`borrow()` / `borrow_mut()`) через неатомарный счетчик во флаге.
- Поэтому передать САМ `RefCell` в другой поток можно (`Send`), но передать ССЫЛКУ `&RefCell` в несколько потоков одновременно НЕЛЬЗЯ (`!Sync`).
- Для безопасной внутренней мутабельности между потоками по ссылки используется `Mutex<T>` или `RwLock<T>`.


---

## Проверь свои знания!

Пройдите короткий тест по типажам Send и Sync:


### 📝 Маркерные типажи Send и Sync


**1. Что гарантирует маркерный типаж `Send` в языке Rust?**

- [ ] Владение объектом типа `T` можно безопасно передать в другой поток
- [ ] Иммутабельные ссылки `&T` можно читать из нескольких потоков одновременно
- [ ] Данные объекта автоматически сериализуются при передаче

**2. Каково математическое и логическое соотношение между типажами `Send` и `Sync`?**

- [ ] `T: Sync` тогда и только тогда, когда `&T: Send`
- [ ] `Send` автоматически отменяет необходимость ключевого слова move
- [ ] `Sync` реализован исключительно для примитивных типов i32 и f64

**3. Почему умный указатель `Rc<T>` НЕ реализует ни `Send`, ни `Sync`?**

- [ ] Счетчик ссылок `Rc` изменяется неатомарными инструкциями, что приведет к состояниям гонки (Data Race) в многопоточной среде
- [ ] Данные `Rc` хранятся только в глобальной памяти только для чтения
- [ ] `Rc` зарезервирован исключительно для декларативных макросов

**4. Почему `RefCell<T>` реализует `Send`, но НЕ реализует `Sync`?**

- [ ] Сам `RefCell` можно переместить в другой поток по значению (`Send`), но его флаг заимствований неатомарен, поэтому разделять `&RefCell` между потоками нельзя (`!Sync`)
- [ ] `RefCell` запрещено использовать внутри функций с аргументами
- [ ] Для `RefCell` компилятор отключает автоматический вызов Drop

