Перейти к основному содержимому
  1. Rust/

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

1096 слов·6 минут· loading · loading · · ·Rust-middle Черновик
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
39 Параллелизм на уровне типов: Всё о маркерных типажах Send и Sync в Rust (текущая)
Не прочитана

Введение
#

Главное обещание 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/2
 1use std::thread;
 2
 3// Шаг 1: Автоматические маркерные типажи Send и Sync
 4struct SafeData {
 5    value: String,
 6}
 7
 8// Структура SafeData автоматически реализует Send и Sync,
 9// так как поле String реализует Send и Sync.
10
11fn main() {
12    let data = SafeData { value: String::from("Привет из главного потока!") };
13
14    // Ключевое слово move передаёт владение `data` внутрь замыкания потока.
15    // Функция thread::spawn требует, чтобы замыкание и его данные реализовывали Send + 'static.
16    let handle = thread::spawn(move || {
17        println!("Данные внутри фонового потока: {}", data.value);
18    });
19
20    handle.join().unwrap();
21}
22

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

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

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

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

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

Замыкание F должно быть:

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

Для передачи владеющих данных в фоновый поток всегда используйте ключевое слово 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/2
 1use std::sync::Arc;
 2use std::thread;
 3
 4// Шаг 1: Почему Rc не является Send, а Arc — является
 5fn main() {
 6    // Arc<T> (Atomic Reference Counting) использует атомарные операции для счетчика ссылок,
 7    // поэтому Arc<T> реализует Send + Sync (при T: Send + Sync) и может передаваться между потоками!
 8    let shared_data = Arc::new(vec![1, 2, 3]);
 9    let data_clone = Arc::clone(&shared_data);
10
11    let handle = thread::spawn(move || {
12        println!("Атомарный Arc в фоновом потоке: {:?}", data_clone);
13    });
14
15    handle.join().unwrap();
16    println!("Главный поток удерживает Arc, сильные ссылки: {}", Arc::strong_count(&shared_data));
17}
18

1. Почему Rc не реализует Send

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

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

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

Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
Цикл статей
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
39 Параллелизм на уровне типов: Всё о маркерных типажах Send и Sync в Rust (текущая)
Не прочитана

Связанные статьи