Понимание примера cppreference на блокировке

Читая о c++ std::lock, я наткнулся на следующий пример из cppreference:

void assign_lunch_partner(Employee &e1, Employee &e2)
{
    static std::mutex io_mutex;
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
    }
 
    // use std::lock to acquire two locks without worrying about 
    // other calls to assign_lunch_partner deadlocking us
    {
        std::lock(e1.m, e2.m);
        std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
        std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
    // Equivalent code (if unique_locks are needed, e.g. for condition variables)
    //        std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
    //        std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
    //        std::lock(lk1, lk2);
    // Superior solution available in C++17
    //        std::scoped_lock lk(e1.m, e2.m);
        {
            std::lock_guard<std::mutex> lk(io_mutex);
            std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
        }
        e1.lunch_partners.push_back(e2.id);
        e2.lunch_partners.push_back(e1.id);
    }
    send_mail(e1, e2);
    send_mail(e2, e1);
}

Хотя я понимаю необходимость сделать io_mutex как static, чтобы его статус распределялся между одновременными вызовами функции assign_lunch_partner (пожалуйста, поправьте меня, если я ошибаюсь), но я не понимаю следующее:

  1. Почему объект lk (lock_guard) был ограничен? Это из-за природы lock_guard?
  2. Тогда, если lk имеет область действия, не означает ли это, что блокировка будет снята после выхода из области действия?
  3. Почему дважды объявляется область видимости lk (lock_guard)? В начале и непосредственно перед обновлением lunch_partners векторов?

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

Drew Dormann 04.08.2023 16:07

@DrewDormann, но я ожидал выполнения параллельных задач в одной и той же области, пока блокировка активна, а не после того, как она вышла за пределы области?

Iron Fist 04.08.2023 16:10

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

Galik 04.08.2023 16:14

@Galik, теперь это имеет смысл, спасибо, так что, я думаю, lk(io_mutex) предназначен только для одновременного std::cout?

Iron Fist 04.08.2023 16:17

Прочтите о шаблоне RAII (более общая тема, чем просто блокировка мьютексов). Это должно ответить на все ваши вопросы.

Marek R 04.08.2023 16:20

@MarekR, спасибо за рекомендацию

Iron Fist 04.08.2023 16:22
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
6
66
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если вам нужно получить две блокировки, вы можете столкнуться с взаимоблокировкой, если кто-то еще попытается получить те же блокировки в обратном порядке. В этом примере показано, как использовать std::lock, чтобы избежать взаимоблокировки. Сразу после блокировки мьютексы принимаются std::lock_guard объектами, чтобы их можно было разблокировать, как только мы покинем область видимости.

Как уже упоминалось, в C++17 это можно сделать проще с помощью std::scoped_lock.

Ответ принят как подходящий

Вы правильно понимаете, почему io_mutex объявлен как статический; это гарантирует, что все одновременные вызовы функции assign_lunch_partner будут синхронизироваться в одном и том же мьютексе.

Теперь давайте ответим на другие ваши вопросы:

  • Почему объект lk (lock_guard) был ограничен? Это из-за природы lock_guard?

Да, это из-за природы std::lock_guard. std::lock_guard получает блокировку в конструкторе и снимает блокировку в деструкторе. Поместив объект std::lock_guard внутрь области видимости (заключенной фигурными скобками {}), блокировка будет снята при выходе из области видимости, поскольку будет вызван деструктор std::lock_guard.

  • Тогда, если lk находится в области действия, не означает ли это, что блокировка будет снята после выхода из области действия?

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

Почему объявления lk с ограниченной областью действия (lock_guard) появляются дважды? В начале и непосредственно перед обновлением векторов launch_partners?

Эти две отдельные защиты блокировки с областью действия синхронизируют разные части кода:

  1. Первый используется для синхронизации вывода на консоль, которая говорит вам, что два сотрудника ждут блокировки. Это гарантирует что если несколько потоков выполняют эту функцию одновременно, их выходы не смешиваются.
  2. Второй используется для синхронизации вывода на консоль, которая говорит вам, что два сотрудника получили замки и готовы обновить векторы lunch_partners. Опять же, это гарантирует, что вывод консоли из нескольких потоков не чередуется.

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

Надеюсь, это проясняет использование защитных блокировок в этом коде!

Другие вопросы по теме