Многопоточность в C++. ​Рекурсивная блокировка мьютекса

Добавлено 22 декабря 2021 в 01:12

Попытка потока заблокировать мьютекс, которым он уже владеет, приводит при использовании std::mutex к ошибке и неопределенному поведению. Но порой бывает нужно, чтобы поток многократно получал один и тот же мьютекс, не разблокируя его предварительно. Для этой цели в стандартной библиотеке C++ предусмотрен класс std::recursive_mutex. Он работает так же, как и std::mutex, за тем лишь исключением, что на один его экземпляр можно из одного и того же потока получить несколько блокировок. Прежде чем мьютекс сможет быть заблокирован другим потоком, нужно будет снять все ранее установленные блокировки, поэтому, если функция lock() вызывается три раза, то три раза должна быть вызвана и функция unlock(). При правильном применении std::lock_guard и std::unique_lock всё это будет сделано за вас автоматически.

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

std::recursive_mutex

Класс recursive_mutex – это примитив синхронизации, который может использоваться для защиты общих данных от одновременного доступа нескольких потоков.

recursive_mutex предлагает эксклюзивную рекурсивную семантику владения:

  • Вызывающий поток владеет recursive_mutex в течение периода времени, который начинается, когда он успешно вызывает либо lock, либо try_lock. В течение этого периода поток может совершать дополнительные вызовы lock или try_lock. Период владения заканчивается, когда поток делает соответствующее количество вызовов unlock.
  • Когда поток владеет recursive_mutex, все остальные потоки будут ждать (для lock) или получать false (для try_lock), если они попытаются захватить recursive_mutex.
  • Максимальное количество раз, которое recursive_mutex может быть заблокирован, в стандарте не указано, но после достижения этого числа вызовы lock будут бросать std::system_error, а вызовы try_lock будут возвращать false.

Поведение программы не определено, если recursive_mutex уничтожается, всё еще будучи заблокированным.

Пример:

#include <iostream>
#include <thread>
#include <mutex>
 
class X {
    std::recursive_mutex m;
    std::string shared;
  public:
    void fun1() {
      std::lock_guard<std::recursive_mutex> lk(m);
      shared = "fun1";
      std::cout << "in fun1, shared variable is now " << shared << '\n';
    }
    void fun2() {
      std::lock_guard<std::recursive_mutex> lk(m);
      shared = "fun2";
      std::cout << "in fun2, shared variable is now " << shared << '\n';
      fun1(); // рекурсивная блокировка здесь становится полезной
      std::cout << "back in fun2, shared variable is " << shared << '\n';
    };
};
 
int main() 
{
    X x;
    std::thread t1(&X::fun1, &x);
    std::thread t2(&X::fun2, &x);
    t1.join();
    t2.join();
}

/*
Возможный вывод:
in fun1, shared variable is now fun1
in fun2, shared variable is now fun2
in fun1, shared variable is now fun1
back in fun2, shared variable is fun1
*/

std::recursive_timed_mutex

std::recursive_timed_mutex работает аналогично тому, как работает std::timed_mutex, но предоставляет возможность многократной блокировки одного мьютекса в одном потоке, как std::recursive_mutex.

Теги

C++ / CppMutex / Мьютексstd::recursive_mutexstd::recursive_timed_mutexSTL / Standard Template Library / Стандартная библиотека шаблоновМногопоточность

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.