сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Многопоточность Rust

Mutex, RwLock и Arc в Rust

Мьютексы и guard

Практическая часть: как защитить общее состояние. Спрашивают про связку Arc<Mutex<T>>, момент снятия блокировки, отравление и выбор между Mutex и RwLock. Плюс любимая ловушка с временным guard.

Стержень: lock возвращает RAII-guard, и блокировка живёт ровно столько, сколько живёт этот guard.

// Формулировки: «зачем Arc<Mutex<T>>?», «когда снимется блокировка?», «что такое отравление мьютекса?»

Два вопроса - две обёртки

Arc отвечает на вопрос «кому принадлежит значение, если потоков несколько» - атомарным подсчётом ссылок. Mutex отвечает на вопрос «кто именно сейчас его меняет» - выдачей эксклюзивного доступа. По отдельности не работает: Arc<i32> не даст записи, а Mutex без Arc не переживёт передачу в потоки.

lock() возвращает MutexGuard - обёртку, через которую видно данные. Пока guard жив, блокировка держится; его деструктор её снимает. Метода unlock нет намеренно: он позволил бы обратиться к данным после освобождения.

// Чтобы сузить критическую секцию, guard кладут в отдельный блок или роняют явным drop(guard) - особенно важно, если дальше идёт долгая работа.

let c = Arc::new(Mutex::new(0));
{
    let mut g = c.lock().unwrap();
    *g += 1;
} // блокировка снята здесь
MutexGuard
RAII-обёртка (resource acquisition is initialization): пока жива, блокировка держится
критическая секция
участок кода под блокировкой

Ловушка временного guard

Классическое зависание в проде: if let Some(v) = m.lock().unwrap().pop() { ... }. Guard здесь - временное значение внутри условия, и живёт оно до конца всей конструкции, а не до конца строки. Любой lock из того же потока внутри ветки встаёт намертво: Mutex нерекурсивный.

Лечится выносом значения в переменную до конструкции: let item = m.lock().unwrap().pop(); - тут guard умирает в конце строки, и дальше замок свободен. То же правило касается match по выражению с lock внутри.

// Проверено на компиляторе: внутри ветки if let try_lock честно возвращает false, а после выхода из блока - снова true.

// плохо: замок держится в теле ветки
if let Some(v) = m.lock().unwrap().pop() { .. }
// хорошо:
let v = m.lock().unwrap().pop();
временное значение
guard, созданный внутри выражения-условия
нерекурсивный мьютекс
повторный захват из того же потока - самоблокировка

Отравление и выбор RwLock

Если поток паникует, удерживая мьютекс, данные могли остаться в нецелостном состоянии. Стандартный Mutex это фиксирует: следующий lock() возвращает Err - мьютекс отравлен. Данные всё же доступны через into_inner у ошибки, но решение «инвариант цел, работаем дальше» принимает человек, а не unwrap по привычке.

RwLock пускает читателей параллельно и выигрывает на перекошенной нагрузке - много чтений, редкие записи. Плата: более сложная синхронизация и риск голодания писателя. Если секция короткая или запись частая, обычный Mutex обычно быстрее и предсказуемее.

отравление
пометка мьютекса после паники под блокировкой
голодание писателя
поток записи долго не получает доступ из-за потока читателей

Как отвечать: «Когда снимается блокировка мьютекса?»

Когда уничтожается guard, который вернул lock: освобождение происходит в его деструкторе. Метода unlock нет специально, иначе можно было бы обратиться к данным после освобождения. Отсюда практика: если после работы с данными идёт что-то долгое, guard роняют явно через drop или кладут в отдельный блок, чтобы сузить критическую секцию. И есть частая ловушка: guard, созданный прямо в условии if let или match, живёт до конца всей конструкции, поэтому повторный lock внутри ветки приводит к самоблокировке - Mutex нерекурсивный.

Ты объясняешь механику через RAII и сразу называешь ловушку, на которой в реальности виснут сервисы. Это ответ уровня «отлаживал такое».

На чём валятся

  • Ищут метод unlock и не понимают, что блокировку снимает деструктор.
  • Держат guard во временном выражении и получают самоблокировку.
  • Глушат отравление unwrap, не подумав о целостности данных.
  • Берут RwLock по умолчанию, хотя запись частая - получают сложность без выигрыша.
  • Держат блокировку на всё тело функции вместо короткой секции.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_mutex1 / 5
    Обработчик держит состояние как Arc<Mutex<Vec<i32>>>. Почему внутри этой ветки второй lock зависает?
    if let Some(v) = state.lock()
        .unwrap().pop()
    {
        let g = state.lock().unwrap();
    }
    A)if let создаёт второй поток исполнения для ветки
    B)pop оставляет мьютекс заблокированным до следующей записи
    C)Mutex в Rust нерекурсивный только в release-сборке
    D)Временный guard из условия живёт до конца всей конструкции
    показать ответ и разбор
    +D)Временный guard из условия живёт до конца всей конструкции

    // разбор: Guard, созданный в условии, — временное значение, и оно живёт до конца всего if let, а не до конца строки. Второй lock из того же потока встаёт на самоблокировку: Mutex нерекурсивный. Лечится вынесением значения в переменную до конструкции — let x = state.lock().unwrap().pop(); — и это одна из самых частых причин зависаний в проде. Если мьютекс лежит в локальной переменной той же функции, борьба за порядок уничтожения вылезет раньше ошибкой заимствования.

  2. #rs_mutex2 / 5
    Поток запаниковал, удерживая мьютекс. Что увидят остальные?
    A)Мьютекс останется заблокированным навсегда, потоки повиснут
    B)lock() вернёт данные как обычно: паника на мьютекс не влияет
    C)lock() вернёт Err: мьютекс отравлен, данные под вопросом
    D)Мьютекс сбросит данные в значение по умолчанию и разблокируется
    показать ответ и разбор
    +C)lock() вернёт Err: мьютекс отравлен, данные под вопросом

    // разбор: Это отравление (poisoning): раз поток упал посреди изменения, данные могли остаться в нецелостном состоянии, и следующий lock честно возвращает Err. Внутрь можно попасть через into_inner у ошибки — если знаешь, что инвариант цел. Правило простое: обрабатывать этот Err осмысленно, а не глушить unwrap.

  3. #rs_mutex3 / 5
    Когда RwLock выгоднее Mutex?
    A)Когда критическая секция длинная: RwLock не блокирует писателей
    B)Когда чтений намного больше записей и они держатся недолго
    C)Когда потоков больше, чем ядер: RwLock не усыпляет их
    D)Когда нужно защитить сразу несколько независимых полей структуры
    показать ответ и разбор
    +B)Когда чтений намного больше записей и они держатся недолго

    // разбор: RwLock пускает читателей параллельно, но платит более сложной синхронизацией и риском голодания писателя. Выигрыш появляется на перекошенной нагрузке — много одновременных чтений при редких записях. Если секция короткая или запись частая, обычный Mutex обычно быстрее и предсказуемее.

  4. #rs_mutex4 / 5
    Что напечатает код?
    let n = Arc::new(Mutex::new(0));
    let hs: Vec<_> = (0..4).map(|_| {
        let n = Arc::clone(&n);
        thread::spawn(move || {
            for _ in 0..1000 {
                *n.lock().unwrap() += 1;
            }
        })
    }).collect();
    for h in hs { h.join().unwrap(); }
    println!("{}", *n.lock().unwrap());
    A)Число меньше 4000: часть инкрементов теряется
    B)Результат недетерминирован — от 1000 до 4000
    C)Паника: второй поток встретит отравленный мьютекс
    D)Ровно 4000: каждый инкремент идёт под блокировкой
    показать ответ и разбор
    +D)Ровно 4000: каждый инкремент идёт под блокировкой

    // разбор: Гонки тут нет по построению: каждый инкремент выполняется под захваченным замком, а join гарантирует, что главный поток читает итог после всех. Потеря инкрементов возможна только там, где счётчик общий и без синхронизации, — но такой код Rust просто не соберёт. Цена известна: 4000 захватов замка, поэтому на горячем счётчике вместо Mutex берут AtomicUsize с fetch_add.

  5. #rs_mutex5 / 5
    Почему Mutex в Rust хранит данные внутри себя, а не защищает их снаружи, как в C?
    A)Так работает быстрее: данные и блокировка лежат рядом и попадают в одну кэш-линию
    B)Так требует стандартная библиотека, чтобы у мьютекса был известный размер при компиляции
    C)Так мьютекс может сам восстановить данные, если поток с блокировкой запаниковал
    D)Так доступ к данным невозможен без захвата блокировки — это гарантирует система типов
    показать ответ и разбор
    +D)Так доступ к данным невозможен без захвата блокировки — это гарантирует система типов

    // разбор: Mutex<T> владеет значением и выдаёт его только через guard, который получается из lock. Забыть взять блокировку физически нельзя: без guard до данных не добраться. В C мьютекс и данные связаны лишь соглашением, и любой участок кода может обратиться к переменной напрямую — отсюда классические гонки, которые тут исключены на уровне типов.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.