Главное обещание Rust в области многопоточности звучит амбициозно: «Fearless Concurrency» (Бесстрашный параллелизм). Компилятор rustc гарантирует полное отсутствие гонок данных (Data Races) на этапе компилляции — программа с потенциальной гонкой данных просто не скомпилируется.
Но как компилятор узнает, какие структуры данных можно передавать между потоками, а какие — категорически нельзя?
Секрет кроется в двух встроенных маркерных типажах (Auto Traits): Send и Sync.
Типажи Send и Sync выводятся компилятором автоматически (Auto Traits). Если все поля вашей структуры реализуют Send и Sync, ваша структура тоже автоматически становится Send и Sync.
Попробуйте запустить код и проверить поведение Send и Sync в интерактивном блоке ниже:
Маркерные типажи Send и Sync
Шаг 1/2
1usestd::thread; 2 3// Шаг 1: Автоматические маркерные типажи Send и Sync
4structSafeData{ 5value: String, 6} 7 8// Структура SafeData автоматически реализует Send и Sync,
9// так как поле String реализует Send и Sync.
1011fnmain(){12letdata=SafeData{value: String::from("Привет из главного потока!")};1314// Ключевое слово move передаёт владение `data` внутрь замыкания потока.
15// Функция thread::spawn требует, чтобы замыкание и его данные реализовывали Send + 'static.
16lethandle=thread::spawn(move||{17println!("Данные внутри фонового потока: {}",data.value);18});1920handle.join().unwrap();21}22
1. Философия Send и Sync
Send означает, что владение объектом типа T можно безопасно передать в другой поток.
Sync означает, что иммутабельные ссылки &T можно безопасно разделять между несколькими потоками одновременно (T: Sync ⟺ &T: Send).
Оба типажа являются маркерными (Auto Traits) и вычитаются компилятором автоматически.
Результат выполнения:
1// ?hidden:start
2usestd::thread; 3// ?hidden:end
4 5// Шаг 2: Ручная реализация Send / Sync для пользовательских оберток над сырыми указателями
6structMyRawWrapper<T>{ 7ptr: *mutT, 8} 910// Сырые указатели (*mut T) по умолчанию НЕ реализуют Send и Sync.
11// Если мы гарантируем потокобезопасность инвариантов вручную, мы используем unsafe impl:
12unsafeimpl<T: Send>SendforMyRawWrapper<T>{}13unsafeimpl<T: Sync>SyncforMyRawWrapper<T>{}1415fnmain(){16letmutval=100i32;17letwrapper=MyRawWrapper{ptr: &mutvalas*muti32};1819lethandle=thread::spawn(move||{20println!("Сырой указатель успешно передан в фоновый поток через unsafe impl Send");21});2223handle.join().unwrap();24}25
2. Ручная реализация unsafe impl Send / Sync
Сырые указатели *const T и *mut T считаются !Send и !Sync, так как компилятор не может проверить правила заимствований.
Обёртки над ними требуют явной инструкции unsafe impl Send только после тщательного аудита потокобезопасности.
Умный указатель Rc<T> (Reference Counting) увеличивает и уменьшает счетчик сильных и слабых ссылок простыми неатомарными операциями (count += 1). Если бы Rc передавался в другой поток, вызов clone() в одном потоке и drop() в другом привели бы к неконтролируемой перезаписи памяти и Data Race.
Для многопоточных сценариев используется Arc<T> (Atomic Reference Counting).
RefCell<T> позволяет изменять данные по иммутабельной ссылке через runtime-счетчик borrow() / borrow_mut().
Вы можете передать сам RefCell по значению в другой поток (Send), так как владение полностью меняет владельца.
Но вы НЕ можете передать ссылку &RefCell в несколько потоков одновременно (!Sync), потому что флаг активных заимствований не защищен атомиками или мьютексом.
Для многопоточной внутренней мутабельности используют Arc<Mutex<T>> или Arc<RwLock<T>>.
Ниже представлен пошаговый пример работы Arc и Mutex:
Потокобезопасность Rc, RefCell, Arc и Mutex
Шаг 1/2
1usestd::sync::Arc; 2usestd::thread; 3 4// Шаг 1: Почему Rc не является Send, а Arc — является
5fnmain(){ 6// Arc<T> (Atomic Reference Counting) использует атомарные операции для счетчика ссылок,
7// поэтому Arc<T> реализует Send + Sync (при T: Send + Sync) и может передаваться между потоками!
8letshared_data=Arc::new(vec![1,2,3]); 9letdata_clone=Arc::clone(&shared_data);1011lethandle=thread::spawn(move||{12println!("Атомарный Arc в фоновом потоке: {:?}",data_clone);13});1415handle.join().unwrap();16println!("Главный поток удерживает Arc, сильные ссылки: {}",Arc::strong_count(&shared_data));17}18
Если бы Rc<T> передавался в другой поток, одновременный clone или drop привел бы к состояниям гонки (Data Race) и порче памяти.
Для многопоточного разделяемого владения строго используется Arc<T>.
Результат выполнения:
1usestd::cell::RefCell; 2usestd::sync::{Arc,Mutex}; 3usestd::thread; 4 5// Шаг 2: Почему RefCell является Send, но НЕ является Sync
6fnmain(){ 7// 1. RefCell<T> реализует Send (его можно полностью переместить в другой поток)
8letcell=RefCell::new(42); 9lethandle=thread::spawn(move||{10*cell.borrow_mut()+=10;11println!("RefCell перемещен и изменен в другом потоке: {}",cell.borrow());12});13handle.join().unwrap();1415// 2. Для одновременного доступа к данным из НЕСКОЛЬКИХ потоков по &T используется Arc<Mutex<T>>
16letmutex_data=Arc::new(Mutex::new(vec!["данные"]));17letmutex_clone=Arc::clone(&mutex_data);1819letthread_handle=thread::spawn(move||{20letmutlock=mutex_clone.lock().unwrap();21lock.push("из фонового потока");22});2324thread_handle.join().unwrap();25println!("Результирующий вектор под Mutex: {:?}",mutex_data.lock().unwrap());26}27
2. Почему RefCell реализует Send, но НЕ реализует Sync
RefCell<T> проверяет динамические заимствования (borrow() / borrow_mut()) через неатомарный счетчик во флаге.
Поэтому передать САМ RefCell в другой поток можно (Send), но передать ССЫЛКУ &RefCell в несколько потоков одновременно НЕЛЬЗЯ (!Sync).
Для безопасной внутренней мутабельности между потоками по ссылки используется Mutex<T> или RwLock<T>.