Многопоточность в C++. Рекурсивная блокировка мьютекса
Попытка потока заблокировать мьютекс, которым он уже владеет, приводит при использовании 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
.