Какой тип мьютекса можно повторно блокировать в том же потоке?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Рекурсивное (повторное) блокирование мьютекса
Вопрос о повторной блокировке одного и того же мьютекса в одном потоке тесно связан с типом мьютекса. Разные типы мьютексов ведут себя по-разному в этой ситуации.
1. std::mutex — обычный мьютекс (не рекурсивный)
std::mutex НЕ поддерживает повторную блокировку в одном потоке. Попытка повторной блокировки приводит к дедлоку:
#include <mutex>
std::mutex m;
void deadlock_example() {
m.lock(); // поток блокирует мьютекс
std::cout << "First lock" << std::endl;
m.lock(); // DEADLOCK! Поток ждёт сам себя
std::cout << "Second lock" << std::endl; // никогда не выполнится
m.unlock();
m.unlock();
}
Это происходит потому, что std::mutex предполагает, что поток владеет мьютексом только один раз. Повторный вызов lock() ждёт, пока мьютекс будет разблокирован, но поток занят самой блокировкой.
2. std::recursive_mutex — рекурсивный мьютекс
std::recursive_mutex можно повторно блокировать из одного и того же потока. Мьютекс отслеживает глубину блокировки (сколько раз один поток заблокировал его) и требует столько же разблокировок:
#include <mutex>
std::recursive_mutex rm;
void recursive_function(int depth) {
rm.lock();
std::cout << "Lock at depth " << depth << std::endl;
if (depth > 0) {
recursive_function(depth - 1); // рекурсивный вызов
}
rm.unlock();
std::cout << "Unlock at depth " << depth << std::endl;
}
int main() {
recursive_function(3);
return 0;
}
Внутренне std::recursive_mutex ведёт счётчик блокировок:
std::recursive_mutex rm;
void example() {
rm.lock(); // счётчик = 1
rm.lock(); // счётчик = 2
rm.lock(); // счётчик = 3
std::cout << "Inside triple lock" << std::endl;
rm.unlock(); // счётчик = 2
rm.unlock(); // счётчик = 1
rm.unlock(); // счётчик = 0, мьютекс свободен
}
3. Типы мьютексов и их характеристики
- std::mutex — обычный мьютекс, не поддерживает повторную блокировку в одном потоке
- std::recursive_mutex — поддерживает повторную блокировку со счётчиком вложенности
- std::timed_mutex — обычный мьютекс с таймаутом, не поддерживает повторную блокировку
- std::recursive_timed_mutex — рекурсивный мьютекс с поддержкой таймаута
4. Примеры использования std::recursive_mutex
Случай 1: Рекурсивные функции
class FileProcessor {
private:
std::recursive_mutex fileMutex;
public:
void processFile(const std::string& filename, int level = 0) {
std::lock_guard<std::recursive_mutex> lock(fileMutex);
std::cout << "Processing: " << filename << std::endl;
// Получение подфайлов (которые тоже нужно обработать)
std::vector<std::string> subfiles = getSubfiles(filename);
for (const auto& subfile : subfiles) {
// Рекурсивный вызов - требует повторной блокировки
processFile(subfile, level + 1);
}
}
};
Случай 2: Классы с методом, вызывающим другой метод
class DataContainer {
private:
std::recursive_mutex dataMutex;
std::vector<int> data;
public:
void addData(int value) {
std::lock_guard<std::recursive_mutex> lock(dataMutex);
data.push_back(value);
}
void processAndAdd(int value) {
std::lock_guard<std::recursive_mutex> lock(dataMutex);
int processed = processValue(value);
addData(processed);
}
private:
int processValue(int v) {
return v * 2;
}
};
5. RAII обёртки для мьютексов
Вместо явного lock()/unlock() используй std::lock_guard или std::unique_lock:
std::recursive_mutex rm;
void safe_example() {
std::lock_guard<std::recursive_mutex> lock(rm);
std::cout << "Mьютекс заблокирован" << std::endl;
}
void timed_lock_example() {
std::recursive_timed_mutex rtm;
std::unique_lock<std::recursive_timed_mutex> lock(rtm, std::chrono::milliseconds(100));
if (lock.owns_lock()) {
std::cout << "Успешно заблокирован" << std::endl;
} else {
std::cout << "Таймаут истёк" << std::endl;
}
}
6. Недостатки recursive_mutex
Производительность: recursive_mutex медленнее обычного мьютекса из-за дополнительной памяти (счётчик, id потока) и операций при каждой блокировке.
Скрытые ошибки:
std::recursive_mutex rm;
int counter = 0;
void increment() {
rm.lock();
counter++;
rm.unlock();
}
void double_increment() {
rm.lock();
increment(); // recursive lock - запутанный код
rm.unlock();
}
7. Рекомендации
Используй std::recursive_mutex когда:
- Действительно нужна рекурсивная блокировка (редко)
- Рефакторить сложно (легаси-код)
Лучше использовать std::mutex когда:
- Возможно избежать рекурсии (чаще всего)
- Критична производительность
- Новый код
// Плохо - использование recursive_mutex
std::recursive_mutex rm;
void processData() {
std::lock_guard<std::recursive_mutex> lock(rm);
processSubData();
}
void processSubData() {
// Неявно требует блокировки от processData
}
// Хорошо - явное разделение
std::mutex m;
void processData() {
std::lock_guard<std::mutex> lock(m);
processSubDataUnlocked();
}
void processSubDataUnlocked() {
// Явно требует, чтобы processData уже заблокировал мьютекс
}
Итого
Ответ на вопрос: std::recursive_mutex — это единственный мьютекс в стандартной библиотеке C++, который можно повторно блокировать из одного потока.
- std::mutex — дедлок при повторной блокировке
- std::recursive_mutex — поддерживает повторную блокировку со счётчиком
- std::timed_mutex — дедлок, но с таймаутом
- std::recursive_timed_mutex — рекурсивная блокировка с таймаутом
Лучше начинать со std::mutex и переходить на recursive_mutex только если абсолютно необходимо.