За счёт чего std::scoped_lock предотвращает взаимную блокировку при захвате нескольких мьютексов?
std::scoped_lock для нескольких мьютексов использует механизм, эквивалентный вызову std::lock: он захватывает их по алгоритму, который при конфликте освобождает уже захваченные мьютексы и повторяет попытку в безопасном порядке. Поэтому два потока могут передавать один и тот же набор мьютексов в разном порядке без взаимной блокировки, если оба используют этот механизм для совместного захвата.
std::scoped_lock также автоматически освобождает все захваченные мьютексы при выходе из области видимости.
Ручной последовательный захват нескольких мьютексов часто приводит к взаимной блокировке: один поток удерживает первый мьютекс и ждёт второй, а другой удерживает второй и ждёт первый. Для решения этой проблемы в стандартной библиотеке появился std::lock, а в C++17 — RAII-обёртка std::scoped_lock, объединяющая безопасный захват и автоматическое освобождение.
Подход устраняет необходимость вручную поддерживать глобальный порядок захвата мьютексов в каждом месте программы.
Рассмотрим два общих ресурса, например счета. Один поток может сначала заблокировать первый ресурс, затем второй, а другой — сначала второй, затем первый. Если оба захвата уже состоялись частично, каждый поток будет бесконечно ждать мьютекс, удерживаемый другим.
Неправильное решение приводит к зависанию, которое обычно трудно воспроизвести: оно зависит от планирования потоков и временных задержек. Простая замена отдельных lock_guard на несколько независимых объектов не устраняет проблему.
При передаче нескольких мьютексов std::scoped_lock применяет алгоритм множественного захвата, основанный на std::lock. Если очередной мьютекс занят, реализация не обязана продолжать удерживать уже захваченные мьютексы: она может освободить их и повторить попытку. Это предотвращает устойчивое состояние циклического ожидания между потоками.
Порядок аргументов в двух вызовах transfer может отличаться: механизм множественного захвата всё равно координирует получение мьютексов. После успешного захвата создаётся RAII-объект, а его уничтожение освобождает все мьютексы.
Гарантия относится к согласованному использованию этого механизма всеми конкурирующими участниками. Если один участок захватывает мьютексы вручную в конфликтующем порядке, использует дополнительные мьютексы или вызывает повторный захват уже принадлежащего потоку мьютекса, взаимная блокировка всё ещё возможна. Один и тот же мьютекс нельзя передавать в std::scoped_lock несколько раз.
std::scoped_lock не делает операции над защищёнными данными атомарными самими по себе: он только защищает критическую секцию. Кроме того, удержание нескольких мьютексов увеличивает область блокировки, поэтому критическую секцию следует делать как можно короче.
В сервисе переводов между счетами периодически возникали зависания. Один обработчик блокировал счёт отправителя, затем счёт получателя, а другой при обратном переводе делал это в обратной последовательности.
Рассматривались два решения. Первый вариант — ввести глобальный порядок по идентификаторам счетов и всегда блокировать мьютексы в этом порядке. Он эффективен и предсказуем, но требует дисциплины во всех местах доступа. Второй вариант — использовать std::scoped_lock для одновременного захвата обоих мьютексов. Он проще для локального кода и снижает риск ошибки, но не защищает от участков программы, которые обходят это правило.
Выбрали std::scoped_lock, а доступ к счетам централизовали через несколько функций. Зависания из-за циклического ожидания исчезли; при этом операции, не требующие обоих счетов, оставили под одним мьютексом, чтобы не увеличивать конкуренцию без необходимости.
std::scoped_lock отсутствие любой взаимной блокировки в программе?Нет. Он предотвращает взаимную блокировку при совместном захвате переданных мьютексов через соответствующий механизм. Взаимная блокировка всё ещё возможна из-за других мьютексов, несовместимого ручного захвата, ожидания внешнего события внутри критической секции или повторного захвата нерекурсивного мьютекса.
std::scoped_lock отличается от нескольких std::lock_guard?Несколько std::lock_guard, созданных последовательно, захватывают мьютексы последовательно и могут образовать цикл ожидания. std::scoped_lock при нескольких аргументах использует алгоритм множественного захвата, а затем управляет временем жизни всех блокировок как единым RAII-объектом.
std::scoped_lock для одного мьютекса?Да, такой вариант допустим и по смыслу похож на std::lock_guard: мьютекс захватывается при создании объекта и освобождается при его уничтожении. Главное преимущество std::scoped_lock проявляется при захвате нескольких мьютексов; для одного мьютекса часто выбирают std::lock_guard или std::unique_lock в зависимости от требуемых возможностей.