В отличие от Go, где горутина — лёгкая (несколько КБ стека, планируется рантаймом), std::thread::spawn в Rust создаёт настоящий поток ОС (по умолчанию 2-8 МБ стека, планируется ядром). Это делает Rust-потоки "тяжелее" горутин, но убирает необходимость в собственном планировщике для синхронного кода.
use std::thread;
let handle = thread::spawn(|| {
for i in 1..5 {
println!("поток: {i}");
}
});
// join() блокирует текущий поток до завершения дочернего
let result = handle.join(); // Result<(), Box<dyn Any + Send>>
if result.is_err() {
println!("дочерний поток запаниковал");
}
Замыкание, переданное в spawn, обязано быть 'static — оно не может заимствовать данные из внешнего стека, потому что порождающий поток может завершиться раньше дочернего. Это заставляет использовать move для передачи владения:
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("{:?}", data); // data перемещена в поток
});
handle.join().unwrap();
// data здесь больше недоступна
Многопоточная безопасность Rust держится на двух маркерных трейтах, которые компилятор проверяет автоматически. Они не содержат методов — это просто "метки" в системе типов.
&T (эквивалентно &T: Send)// Оба трейта — auto traits: выводятся автоматически из полей типа
// Rc<T> НЕ Send и НЕ Sync — счётчик не атомарный
// Arc<T> — Send + Sync, если T: Send + Sync
// RefCell<T> — Send, но НЕ Sync (borrow-flag не атомарный)
// Mutex<T> — Send + Sync, если T: Send (даже если T не Sync!)
fn assert_send<T: Send>() {}
assert_send::<std::sync::Arc<i32>>(); // компилируется
// assert_send::<std::rc::Rc<i32>>(); // ошибка компиляции!
Send/Sync, встроенную в сигнатуры thread::spawn.
Идиома Rust (унаследованная от Go/CSP): "не общайтесь через разделяемую память — разделяйте память через общение". std::sync::mpsc (multi-producer, single-consumer) даёт канал для передачи владения данными между потоками.
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone(); // multi-producer — клонируем Sender
thread::spawn(move || {
tx.send("из потока 1").unwrap();
});
thread::spawn(move || {
tx2.send("из потока 2").unwrap();
});
for received in rx { // rx как итератор — блокируется, пока все tx не задропаны
println!("получено: {received}");
}
Канал закрывается автоматически, когда все Sender выходят из области видимости (Drop) — rx.recv() тогда вернёт Err. mpsc::sync_channel(n) создаёт канал с ограниченной ёмкостью буфера — send() блокируется, если буфер заполнен (backpressure).
Принципиальное API-отличие от большинства языков: в Rust Mutex<T> — это обёртка над данными, а не отдельный примитив рядом с данными. Доступ к содержимому возможен только через lock(), который возвращает MutexGuard — тип, реализующий Deref/DerefMut и автоматически освобождающий блокировку через Drop.
use std::sync::Mutex;
let m = Mutex::new(5);
{
let mut guard = m.lock().unwrap(); // unwrap — poisoning, см. ниже
*guard += 1;
} // Drop у guard освобождает блокировку — забыть Unlock() невозможно
println!("{:?}", m);
Компилятор физически не даст прочитать данные под Mutex без блокировки — в отличие от Go, где mu.Lock() и защищаемые данные существуют раздельно, и ничто не мешает случайно обратиться к полю структуры, забыв про lock.
MutexGuard, мьютекс помечается как "отравленный" (poisoned). Последующие lock() будут возвращать Err — по умолчанию нужно явно решать, восстанавливать данные (into_inner()) или паниковать дальше.
Стандартная комбинация для расшаривания изменяемых данных между потоками: Arc даёт совместное владение с потокобезопасным счётчиком, Mutex — эксклюзивный доступ к содержимому.
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("итог: {}", *counter.lock().unwrap()); // 10
Как и sync.RWMutex в Go, RwLock допускает много одновременных читателей ИЛИ одного писателя. Выгоден в read-heavy сценариях, где блокировка на чтение не должна становиться узким местом.
use std::sync::RwLock;
let lock = RwLock::new(vec![1, 2, 3]);
{
let r1 = lock.read().unwrap();
let r2 = lock.read().unwrap(); // несколько read-guard'ов одновременно — ОК
println!("{:?} {:?}", r1, r2);
}
{
let mut w = lock.write().unwrap(); // эксклюзивный доступ
w.push(4);
}
| Mutex<T> | RwLock<T> | |
|---|---|---|
| Читатели одновременно | нет, эксклюзив всегда | да, много читателей |
| Оверхед | минимальный | выше (учёт читателей/писателей) |
| Когда выгоден | частые записи, простые данные | чтение >> запись |
| Риск starvation писателя | нет | да, при потоке читателей |
Send/Sync гарантируют отсутствие data races, но не отсутствие взаимных блокировок — deadlock остаётся логической ошибкой, которую компилятор не ловит.
use std::sync::Mutex;
let m = Mutex::new(5);
let _g1 = m.lock().unwrap();
let _g2 = m.lock().unwrap(); // deadlock: Mutex не реентерабелен,
// _g1 ещё жив, поток блокируется навсегда
// Классический deadlock из двух потоков с обратным порядком блокировок:
// поток A: lock(mutex1) → lock(mutex2)
// поток B: lock(mutex2) → lock(mutex1)
// → циклическое ожидание
MutexGuard явными блоками { } — чем короче удержание, тем меньше contention.await в асинхронном коде (см. тему 15) — используйте tokio::sync::Mutex, если блокировка нужна внутри async-функцииstd::sync::atomic) быстрее, чем Mutex<i64> (подробнее — тема 14)