Какие знаешь мьютексы?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Какие знаешь мьютексы?
Мьютексы (mutual exclusion) — это основа синхронизации в многопоточном коде. За 10+ лет я работал с разными видами мьютексов и их комбинациями. Опишу каждый и как их правильно использовать.
1. std::mutex — базовый мьютекс
Самый простой вид. Может быть в двух состояниях: заблокирован или разблокирован.
#include <mutex>
#include <thread>
#include <iostream>
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 1000; i++) {
std::lock_guard<std::mutex> lock(mtx); // Автоматическая блокировка
counter++; // Критичный раздел
} // Автоматическая разблокировка
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter: " << counter << std::endl; // 2000 (безопасно)
}
Без мьютекса результат был бы случайным (race condition):
// ОПАСНО!
void increment_unsafe() {
for (int i = 0; i < 1000; i++) {
counter++; // NOT atomic!
// На самом деле:
// 1. Читаем counter
// 2. Увеличиваем
// 3. Пишем обратно
// Между шагами другой поток может изменить counter!
}
}
// Результат может быть 1500, 1234, 1999, не 2000
2. std::lock_guard vs std::unique_lock
std::lock_guard — простой и быстрый
std::mutex mtx;
void func() {
std::lock_guard<std::mutex> lock(mtx); // Locked
// Критичный раздел
} // Unlocked автоматически
Преимущества:
- Простота
- Нулевые накладные расходы
- RAII гарантия
Недостатки:
- Нельзя явно разблокировать
- Нельзя использовать условные переменные
std::unique_lock — гибкий
std::mutex mtx;
std::condition_variable cv; // Условная переменная
bool ready = false;
void wait_for_signal() {
std::unique_lock<std::mutex> lock(mtx); // Locked
// Ждём сигнала от другого потока
cv.wait(lock, []{ return ready; });
std::cout << "Signal received!" << std::endl;
} // Unlocked автоматически
void send_signal() {
{
std::unique_lock<std::mutex> lock(mtx);
ready = true;
} // Unlocked перед notify
cv.notify_one(); // Пробуждаем один ждущий поток
}
Преимущества:
- Явная разблокировка (unlock())
- Поддержка condition_variable
- Defer lock (задержанная блокировка)
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// lock еще не захвачен
lock.lock(); // Явная блокировка
lock.unlock(); // Явная разблокировка
lock.lock(); // Можем заблокировать снова
3. std::recursive_mutex — рекурсивный мьютекс
Один поток может заблокировать мьютекс несколько раз.
std::recursive_mutex rmtx;
void process(int level) {
std::lock_guard<std::recursive_mutex> lock(rmtx);
std::cout << "Level " << level << std::endl;
if (level < 3) {
process(level + 1); // Рекурсивный вызов
// Нет deadlock'а!
}
}
int main() {
process(1);
// Level 1
// Level 2
// Level 3
}
ВАЖНО: Избегайте рекурсивных мьютексов если можно. Это признак плохого дизайна.
// ❌ ПЛОХО
class Counter {
private:
std::recursive_mutex mtx; // Признак проблемы
int value = 0;
public:
void increment() {
std::lock_guard<std::recursive_mutex> lock(mtx);
value++;
}
int get_double() {
std::lock_guard<std::recursive_mutex> lock(mtx);
return value * 2;
}
};
// ✅ ХОРОШО
class Counter {
private:
std::mutex mtx;
int value = 0;
// Приватный метод без блокировки
int get_value_unlocked() const {
return value;
}
public:
void increment() {
std::lock_guard<std::mutex> lock(mtx);
value++;
}
int get_double() {
std::lock_guard<std::mutex> lock(mtx);
return get_value_unlocked() * 2; // Уже заблокировано
}
};
4. std::timed_mutex и std::recursive_timed_mutex
Мьютексы с timeout.
std::timed_mutex tmtx;
bool try_critical_section() {
// Пытаемся заблокировать на 1 секунду
if (tmtx.try_lock_for(std::chrono::seconds(1))) {
std::cout << "Lock acquired!" << std::endl;
// Критичный раздел
tmtx.unlock();
return true;
} else {
std::cout << "Lock timeout!" << std::endl;
return false;
}
}
bool try_until_time() {
auto deadline = std::chrono::high_resolution_clock::now() + std::chrono::seconds(5);
if (tmtx.try_lock_until(deadline)) {
// Успешно заблокировали до deadline
tmtx.unlock();
return true;
}
return false;
}
5. std::shared_mutex (читатели-писатели)
Несколько читателей могут одновременно захватить мьютекс, но писатель требует эксклюзивного доступа.
std::shared_mutex rwmtx; // Reader-Writer mutex
int shared_data = 0;
void reader(int id) {
std::shared_lock<std::shared_mutex> lock(rwmtx);
std::cout << "Reader " << id << " sees: " << shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
void writer(int id, int value) {
std::unique_lock<std::shared_mutex> lock(rwmtx);
std::cout << "Writer " << id << " sets: " << value << std::endl;
shared_data = value;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
int main() {
std::thread readers[3], writers[2];
// Запускаем 3 читателя
for (int i = 0; i < 3; i++) {
readers[i] = std::thread(reader, i);
}
// Запускаем 2 писателя
for (int i = 0; i < 2; i++) {
writers[i] = std::thread(writer, i, i * 10);
}
for (int i = 0; i < 3; i++) readers[i].join();
for (int i = 0; i < 2; i++) writers[i].join();
}
Timeline:
Reader 0, Reader 1, Reader 2 читают одновременно (shared lock)
↓
Writer 0 ждёт (нужен exclusive lock)
↓
Reader 0, 1, 2 завершают
↓
Writer 0 захватывает и пишет
↓
Writer 1 ждёт
↓
Writer 0 завершает
↓
Writer 1 захватывает и пишет
6. std::condition_variable
Уведомление между потоками.
std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;
void producer() {
for (int i = 0; i < 5; i++) {
{
std::unique_lock<std::mutex> lock(mtx);
queue.push(i);
std::cout << "Produced: " << i << std::endl;
} // lock释放
cv.notify_one(); // Пробуждаем один потребителя
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer(int id) {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// Ждём, пока очередь не пуста
cv.wait(lock, []{ return !queue.empty(); });
if (queue.empty()) break;
int value = queue.front();
queue.pop();
std::cout << "Consumer " << id << " consumed: " << value << std::endl;
}
}
int main() {
std::thread prod(producer);
std::thread cons1(consumer, 1);
std::thread cons2(consumer, 2);
prod.join();
// ...
}
7. Deadlock и как его избежать
Deadlock — ситуация, когда потоки ждут друг друга бесконечно.
// ❌ ОПАСНО — возможен deadlock
std::mutex mtx1, mtx2;
void thread1_func() {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lock2(mtx2); // Ждём mtx2
}
void thread2_func() {
std::lock_guard<std::mutex> lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lock1(mtx1); // Ждём mtx1
}
// thread1 владеет mtx1, ждёт mtx2
// thread2 владеет mtx2, ждёт mtx1
// DEADLOCK!
Решение: std::lock с несколькими мьютексами
// ✅ БЕЗОПАСНО
std::mutex mtx1, mtx2;
void thread1_func() {
std::lock(mtx1, mtx2); // Блокируем оба сразу в безопасном порядке
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// Логика
}
void thread2_func() {
std::lock(mtx1, mtx2); // Тот же порядок — no deadlock
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// Логика
}
8. Практический выбор мьютекса
┌─────────────────────┬──────────────────┬────────────────────┐
│ Тип мьютекса │ Использование │ Производительность│
├─────────────────────┼──────────────────┼────────────────────┤
│ std::mutex │ Общие случаи │ Быстро │
│ std::timed_mutex │ С timeout │ Средне │
│ std::recursive_mx. │ Редко (плохо) │ Медленно │
│ std::shared_mutex │ Много читателей │ Зависит от нагрузки│
└─────────────────────┴──────────────────┴────────────────────┘
9. Lock-free альтернатива
// Вместо мьютекса можно использовать атомы
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; i++) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter: " << counter << std::endl; // 2000
}
Вывод
Правила использования мьютексов:
- Используй
std::lock_guardпо умолчанию - Используй
std::unique_lockесли нужна condition_variable - Избегай
std::recursive_mutex(признак плохого дизайна) - Используй
std::shared_mutexдля читателей-писателей - Блокируй минимальное количество кода
- Избегай вложенных блокировок (или используй
std::lock) - Рассмотри lock-free структуры для критичного по производительности кода