ПрограммированиеBackend разработчик

Какие способы синхронизации потоков предоставляет стандартная библиотека Rust? Как выбирать между ними и какие «тонкие места» обязательно нужно учитывать?

Проходите собеседования с ИИ помощником Hintsage

Ответ

В стандартной библиотеке Rust представлены основные примитивы синхронизации для безопасной работы с многопоточностью:

  • Mutex — обеспечивает взаимное исключение для доступа к данным из нескольких потоков;
  • RwLock — позволяет одновременно несколько читателей (read), но только одного писателя (write);
  • Condvar — примитив условной переменной для организации пробуждения потоков по событию;
  • Atomic типы (AtomicBool, AtomicUsize и др.) — операции чтения/записи без блокировок, на уровне аппаратуры;
  • Arc (Atomic Reference Counted) — подсчет ссылок с потокобезопасностью, для совместного владения объектами.

Выбор:

  • Если одновременно требуется только чтение — используйте RwLock (эффективнее, чем Mutex).
  • Для простого синхронизированного доступа одного потока — Mutex.
  • Для синхронизации по сигналу (например, «ожидать нового элемента») — Condvar.
  • Для атомарных счетчиков/флагов — Atomic.
  • Для совместного владения (многопоточного) — Arc.

Пример:

use std::sync::{Arc, Mutex}; use std::thread; fn main() { 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!("Result: {}", *counter.lock().unwrap()); }

Вопрос с подвохом

Вопрос: Гарантирует ли Rust, что использование Mutex<T> полностью избавляет от deadlock'ов за счёт проверки на этапе компиляции?

Ответ: Нет. Rust обеспечивает безопасный доступ к данным через владение и borrow-checker, но не защищает от deadlock'ов на уровне языка. Мёртвые блокировки возникают чисто логически при нарушении порядка захвата нескольких Mutex или их рекурсивном захвате. Пример:

use std::sync::Mutex; let lock1 = Mutex::new(0); let lock2 = Mutex::new(0); // Поток 1: lock1 -> lock2, Поток 2: lock2 -> lock1 ⇒ deadlock

История

Проект потоковой обработки данных испытывал случайные "подвисания". Оказалось, разработчики использовали вложенные Mutex (Mutex внутри Mutex) без строгого порядка захвата, что приводило к взаимным блокировкам (deadlock), снимаемых только принудительным завершением процесса.

История

В крупном сервисе массово мигрировали с Mutex<Option<T>> на RwLock<T>, не учли, что writable lock может быть дольше, чем lock на чтение. В пиковой нагрузке это обернулось задержками обработки до десятков секунд из-за очередей на write.

История

Программисты пытались экономить на потоках, "пушили" Arc<Mutex<_>> в сотни потоков. Из-за тонкостей работы планировщика и переиспользования mutex блокировки появлялись удивительные взаимные ожидания — производительность снизилась в 5 раз!