Программирование C++МногопоточностьРазработчик C++ многопоточных систем

Как следует рассуждать о переходе от совместной блокировки std::shared mutex к исключительной, если условие...

Как следует рассуждать о переходе от совместной блокировки std::shared_mutex к исключительной, если условие проверено под shared lock?

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

Краткий ответ

У std::shared_mutex нет атомарного перехода от совместной блокировки к исключительной. Если поток отпускает shared lock, а затем запрашивает exclusive lock, между этими действиями другой поток может изменить защищаемое состояние, поэтому условие необходимо проверить повторно уже под исключительной блокировкой.

Исторический контекст

Блокировки читателей-писателей разделяют доступ на совместный для чтения и исключительный для записи. Это позволяет нескольким читателям работать параллельно, сохраняя единственного владельца при изменении данных.

Стандартный std::shared_mutex предоставляет независимые операции захвата и освобождения этих режимов, но не определяет операцию безопасного повышения режима. Такое ограничение явно отделяет простую блокировку от более сложных протоколов управления состоянием.

Постановка проблемы

Типичный сценарий выглядит так: поток читает состояние под shared lock, обнаруживает необходимость изменения, освобождает совместную блокировку и пытается получить исключительную. В момент между освобождением и новым захватом другой поток может изменить данные или выполнить ту же операцию.

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

Подробное решение

Безопасный общий протокол состоит в том, чтобы после получения исключительной блокировки заново проверить условие. Если условие больше не выполняется, поток должен отказаться от изменения; если оно всё ещё выполняется, изменение можно выполнить.

#include <mutex> #include <shared_mutex> std::shared_mutex mutex; int value = 0; void update() { { std::shared_lock read_lock(mutex); if (value != 0) return; } std::unique_lock write_lock(mutex); if (value == 0) value = 42; }

Такой переход не является атомарным: другой поток может получить исключительную блокировку первым. Однако повторная проверка под этой блокировкой сохраняет корректность.

Если операция изменения нужна почти всегда после проверки, выгоднее сразу использовать unique_lock: это проще и устраняет окно между режимами, но уменьшает параллелизм чтения. Если проверка дорогая, а изменение редкое, схема с повторной проверкой может быть эффективнее, однако она усложняет логику и иногда требует цикла повторных попыток.

Нельзя просто удерживать shared lock и одновременно пытаться получить exclusive lock: это может привести к взаимной блокировке, поскольку текущий поток сам препятствует получению исключительного режима.

Ситуация из практики

В кэше несколько потоков совместно проверяют наличие записи. При отсутствии записи каждый пытается перейти к записи. Наивная схема с отпусканием shared lock приводит к тому, что несколько потоков одновременно увидят отсутствие записи и начнут конкурировать за инициализацию.

Вариант с немедленным unique_lock прост и надёжен, но блокирует чтение на время всей проверки. Вариант с отпусканием shared lock, последующим unique_lock и повторной проверкой сохраняет параллельность чтения, но только один поток выполнит инициализацию; остальные обнаружат, что запись уже появилась.

Для такого кэша обычно выбирают второй вариант, если проверка существенно чаще самой инициализации. Повторная проверка под исключительной блокировкой устраняет дублирование и сохраняет корректность без предположения о наличии атомарного повышения режима.

Что кандидаты часто упускают

  1. Достаточно ли после получения unique lock проверить только наличие данных, не повторяя остальные условия?

Нет. Нужно повторить все проверки, от которых зависит корректность изменения. Между первоначальным чтением и получением исключительной блокировки могли измениться не только наличие записи, но и версия, права доступа, лимиты или связанные поля.

  1. Можно ли считать try_lock способом безопасного повышения shared lock?

Нет. try_lock лишь сообщает, удалось ли получить исключительную блокировку в конкретный момент. Он не делает переход атомарным и не сохраняет валидность результата, полученного под совместной блокировкой. После успешного try_lock состояние всё равно следует проверить заново.

  1. Почему атомарное поле не всегда заменяет повторную проверку под блокировкой?

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