Ответ
std::mutex (mutual exclusion, взаимное исключение) — это базовый примитив синхронизации из стандартной библиотеки C++ (<mutex>), предназначенный для защиты общих данных от одновременного доступа нескольких потоков, предотвращая состояния гонки (race conditions).
Базовый пример (НЕ рекомендуется для production):
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex g_console_mutex; // Мьютекс для защиты std::cout
int g_shared_counter = 0;
std::mutex g_counter_mutex;
void unsafe_increment() {
// ПРОБЛЕМА: между проверкой и модификацией может вклиниться другой поток
if (g_shared_counter < 100) {
// Имитация работы
std::this_thread::sleep_for(std::chrono::milliseconds(10));
++g_shared_counter;
}
}
void safe_increment() {
// Правильный подход: блокируем мьютекс на ВСЁ время работы с разделяемой переменной
std::lock_guard<std::mutex> lock(g_counter_mutex);
if (g_shared_counter < 100) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
++g_shared_counter;
// Защищённый вывод в консоль
std::lock_guard<std::mutex> console_lock(g_console_mutex);
std::cout << "Thread " << std::this_thread::get_id()
<< ": counter = " << g_shared_counter << std::endl;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(safe_increment); // Используем безопасную версию
}
for (auto& t : threads) t.join();
std::cout << "Final counter: " << g_shared_counter << std::endl; // Всегда <= 100
return 0;
}
Ключевые классы для работы с мьютексами:
std::lock_guard— простейшая RAII-обёртка. Захватывает мьютекс в конструкторе и освобождает в деструкторе. Нельзя разблокировать явно или повторно захватить.std::unique_lock— более гибкая обёртка. Позволяет:- Откладывать захват (
std::defer_lock). - Явно вызывать
lock()/unlock(). - Работать с условными переменными (
std::condition_variable). - Передавать владение мьютексом.
- Откладывать захват (
Пример с std::unique_lock и условной переменной:
std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
void producer() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
data_ready = true;
}
cv.notify_one(); // Сигнализируем потребителю
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return data_ready; }); // Освобождает мьютекс и ждёт notify
// После пробуждения мьютекс снова захвачен
std::cout << "Data is ready!" << std::endl;
}
Важные нюансы:
- Взаимная блокировка (Deadlock): Возникает, когда два или более потока ждут друг друга. Лечится с помощью
std::lock(), который захватывает несколько мьютексов атомарно. - Производительность: Мьютекс — «тяжёлый» примитив. Для простых атомарных операций над одним типом данных предпочтительнее использовать
std::atomic. - Иерархия мьютексов (C++17):
std::mutex→std::timed_mutex(с таймаутами) →std::recursive_mutex(повторно захватываемый тем же потоком) →std::shared_mutex(C++17, для режима «много читателей / один писатель»).
Ответ 18+ 🔞
А, ну вот, мой друг, подходи, садись на пенёк, слушай байку про мьютексы. Это ж, блядь, как в очереди за водкой в девяностые — если все сразу лезут, бутылёв не хватит, и всем пиздец. Так и с потоками: лезут они все разом к одной переменной, а она одна, хитрая жопа. Получается состояние гонки, или, как умные говорят, race condition. А итог — программа работает так, будто её пьяный ёжик писал.
Вот смотри, самый простой пример, который в продакшн совать — это как сунуть хуй в розетку. Опасно и больно.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex g_console_mutex; // Это наш охранник у двери в буфет
int g_shared_counter = 0;
std::mutex g_counter_mutex; // А это охранник у самой водки
void unsafe_increment() {
// ПРОБЛЕМА: щас один поток проверит, что счётчик меньше 100, а пока он зевает, другой уже успеет туда впиздюрить и увеличить. Первый проснётся — а ему уже ёперный театр, он счётчик ломает.
if (g_shared_counter < 100) {
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Зевнул, блядь
++g_shared_counter;
}
}
А вот как надо, по-человечески, с охраной:
void safe_increment() {
// Берём нашего верного цепного пса — lock_guard. Он как только родился (конструктор), сразу мьютекс хватает. А как помрёт (деструктор) — отпускает. Удобно, ёпта.
std::lock_guard<std::mutex> lock(g_counter_mutex);
if (g_shared_counter < 100) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
++g_shared_counter;
// А вывод в консоль — это отдельный буфет, ему свой охранник нужен, а то все строчки в кучу перемешаются.
std::lock_guard<std::mutex> console_lock(g_console_mutex);
std::cout << "Thread " << std::this_thread::get_id()
<< ": counter = " << g_shared_counter << std::endl;
}
}
Ну и главная функция, где мы этих потоков, как тараканов, запускаем:
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(safe_increment); // Запускаем безопасных ребят
}
for (auto& t : threads) t.join(); // Ждём, пока все отпьют
std::cout << "Final counter: " << g_shared_counter << std::endl; // Всегда будет не больше 100, а не овердохуища.
return 0;
}
А теперь про обёртки, это важно, как выбор между тупым топором и швейцарским ножом.
-
std::lock_guard— это тупой, но надёжный топор. Взял в руки — уже рубишь. Из рук выронил (деструктор) — перестал. Разблокировать пораньше не получится, ядрёна вошь. Зато просто и дуракоустойчиво. -
std::unique_lock— это уже тот самый швейцарский нож с десятью лезвиями. Можешь взять, но не сразу открыть (defer_lock). Можешь открыть-закрыть (lock()/unlock()) когда захочешь. Идеален для условных переменных — это когда поток должен ждать, пока другой поток не крикнет "водку привезли!".
Вот, полюбуйся на магию ожидания:
std::mutex mtx;
std::condition_variable cv;
bool data_ready = false; // Флаг, что водка на столе
void producer() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // Идём за водкой
{
std::lock_guard<std::mutex> lock(mtx); // Зашли на кухню
data_ready = true; // Поставили бутыль
} // lock_guard помер, мьютекс отпустили
cv.notify_one(); // Орём на весь дом: "ГОТОВО, ПИДАРАСЫ!"
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx); // Взяли нож в руки
// А тут волшебство: wait() отпускает мьютекс и засыпает, не тратя процессорное время.
// Ждёт, пока не придёт notify. А когда придёт — автоматически снова хватает мьютекс и проверяет условие.
cv.wait(lock, []{ return data_ready; });
// Условие true, мьютекс у нас в руках, можно действовать.
std::cout << "Data is ready!" << std::endl; // Ура, пьём!
}
Ну и напоследок, лайфхаки, чтобы не обосраться:
- Взаимная блокировка (Deadlock): Это когда два потока ждут друг друга, как два упрямых козла на мосту. Один держит мьютекс А и хочет Б, другой держит Б и хочет А. И стоят так до скончания времён. Лечится атомарным захватом через
std::lock(mtx1, mtx2). - Производительность: Мьютекс — штука тяжёлая, как чугунный мост. Если тебе нужно просто атомарно увеличить счётчик, бери
std::atomic— это как быстрый и точный укол, а не кувалда. - Иерархия: В C++17 мьютексы обзавелись роднёй. Есть
timed_mutex— можно стучаться с таймаутом ("откроешь через 5 сек или пошёл нахуй").recursive_mutex— позволяет одному потоку захватить себя несколько раз (осторожно, можно самого себя задушить). И король —shared_mutex:- Много читателей — могут заходить одновременно, как зрители в кино.
- Один писатель — когда он пришёл, всех выгоняют и закрывают дверь. Идеально для данных, которые часто читают, но редко меняют.
Вот и вся философия. С мьютексом — как с последней пачкой сигарет в военкомате: или прячь и защищай, или будь готов, что тебе на всех не хватит.
Видео-ответы
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶