В чём разница между мьютексом и семафором?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Мьютекс vs Семафор
Определения
Мьютекс (Mutex) — взаимное исключение. Примитив синхронизации, который может находиться в двух состояниях: заблокирован (кем-то занят) или разблокирован (свободен). Только один поток может владеть мьютексом одновременно.
Семафор (Semaphore) — счётный примитив синхронизации. Имеет внутренний счётчик, который контролирует доступ к ресурсу. Несколько потоков могут одновременно работать с ресурсом, если счётчик > 0.
Основные различия
| Характеристика | Мьютекс | Семафор |
|---|---|---|
| Тип | Бинарный (2 состояния) | Счётный (N состояний) |
| Владелец | Да (поток, заблокировавший) | Нет (не привязан к потоку) |
| Одновременный доступ | 1 поток | N потоков |
| Механизм | Lock/Unlock | Wait/Signal (P/V) |
| Рекурсивность | Есть recursive_mutex | Нет |
| Производительность | Быстрее | Медленнее |
Мьютекс подробно
Концепция
Мьютекс работает как дверь в комнату:
- Только один человек может быть в комнате одновременно
- Если комната занята, остальные ждут снаружи
- Когда человек выходит, он открывает дверь для следующего
Использование в C++
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
int counter = 0; // Общий ресурс
mutex mtx; // Мьютекс для защиты
void increment() {
for (int i = 0; i < 10000; ++i) {
mtx.lock(); // Попытка захватить мьютекс
counter++; // Критическая секция
mtx.unlock(); // Освобождаем мьютекс
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "Counter: " << counter << endl; // 20000 (безопасно!)
return 0;
}
RAII подход с lock_guard
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
int counter = 0;
mutex mtx;
void increment() {
for (int i = 0; i < 10000; ++i) {
lock_guard<mutex> guard(mtx); // Захват в конструкторе
counter++; // Критическая секция
// Освобождение в деструкторе — гарантировано!
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "Counter: " << counter << endl; // 20000
return 0;
}
Рекурсивный мьютекс
#include <mutex>
using namespace std;
recursive_mutex rmtx; // Один поток может захватить несколько раз
class Counter {
public:
void increment() {
lock_guard<recursive_mutex> guard(rmtx);
// Можно вызвать другую функцию, которая тоже захватывает rmtx
incrementHelper();
}
private:
void incrementHelper() {
lock_guard<recursive_mutex> guard(rmtx); // OK! Один поток
// Логика
}
};
Семафор подробно
Концепция
Семафор работает как парковка с N местами:
- Если мест свободно, машина может припарковаться
- Если мест нет, машина ждёт, пока кто-то не уедет
- Несколько машин могут припарковаться одновременно (до N)
Использование в C++ (C++20)
#include <semaphore>
#include <thread>
#include <iostream>
using namespace std;
counting_semaphore<3> sem(3); // Семафор с начальным значением 3
void worker(int id) {
for (int i = 0; i < 3; ++i) {
sem.acquire(); // Ждём, если счётчик = 0
cout << "Thread " << id << " acquired resource" << endl;
// Работаем с ресурсом (макс 3 одновременно)
this_thread::sleep_for(chrono::milliseconds(100));
cout << "Thread " << id << " releasing resource" << endl;
sem.release(); // Увеличиваем счётчик
}
}
int main() {
thread threads[5];
for (int i = 0; i < 5; ++i)
threads[i] = thread(worker, i);
for (auto& t : threads)
t.join();
return 0;
}
Вывод: одновременно работают максимум 3 потока!
Бинарный семафор (эквивалент мьютекса)
#include <semaphore>
#include <thread>
using namespace std;
binary_semaphore bin_sem(1); // Бинарный семафор: 0 или 1
void worker() {
bin_sem.acquire(); // Ждём, если = 0
cout << "Acquired" << endl;
// Критическая секция
bin_sem.release(); // Устанавливаем = 1
}
Практические примеры
Пример 1: Защита очереди (мьютекс)
#include <queue>
#include <mutex>
#include <thread>
using namespace std;
queue<int> q;
mutex q_mutex;
void producer() {
for (int i = 0; i < 10; ++i) {
lock_guard<mutex> guard(q_mutex);
q.push(i);
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
lock_guard<mutex> guard(q_mutex);
if (!q.empty()) {
int val = q.front();
q.pop();
cout << "Consumed: " << val << endl;
}
}
}
Пример 2: Пул потоков с ограничением (семафор)
#include <semaphore>
#include <thread>
#include <vector>
using namespace std;
const int MAX_CONCURRENT = 3; // Максимум 3 потока одновременно
counting_semaphore<MAX_CONCURRENT> pool_sem(MAX_CONCURRENT);
void task(int id) {
pool_sem.acquire(); // Получаем слот
cout << "Task " << id << " started" << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "Task " << id << " finished" << endl;
pool_sem.release(); // Освобождаем слот
}
int main() {
vector<thread> threads;
for (int i = 0; i < 10; ++i) // 10 задач
threads.emplace_back(task, i);
for (auto& t : threads)
t.join();
return 0;
}
Когда использовать
Используйте мьютекс:
- Для защиты критической секции кода
- Когда только один поток должен работать с ресурсом
- Для простой синхронизации
- Когда нужна гарантия, что поток, захративший мьютекс, его освободит
Используйте семафор:
- Для ограничения количества потоков, доступ к ресурсу (пул потоков)
- Когда N потоков могут работать с ресурсом одновременно
- Для сигнализации между потоками
- Для задач типа "producer-consumer"
Проблемы и deadlock
Deadlock с мьютексами
#include <mutex>
using namespace std;
mutex m1, m2;
void thread1_func() {
lock_guard<mutex> lg1(m1);
this_thread::sleep_for(chrono::milliseconds(1));
lock_guard<mutex> lg2(m2); // DEADLOCK! thread1 ждёт m2
}
void thread2_func() {
lock_guard<mutex> lg2(m2);
this_thread::sleep_for(chrono::milliseconds(1));
lock_guard<mutex> lg1(m1); // DEADLOCK! thread2 ждёт m1
}
// Решение: всегда захватываем мьютексы в одном порядке!
Производительность
Мьютекс:
- Быстрый на обычном оборудовании
- Минимальный оверхед
- Лучше для часто меняющихся данных
Семафор:
- Медленнее из-за счётчика
- Больше оверхеда на операции
- Лучше для управления пулом ресурсов
Современная C++ практика
C++17+:
#include <mutex>
using namespace std;
mutex mtx;
shared_mutex smtx; // Для одновременного чтения, исключительного письма
int shared_data = 0;
void writer() {
unique_lock<shared_mutex> lock(smtx); // Исключительный доступ
shared_data++; // Безопасное письмо
}
void reader() {
shared_lock<shared_mutex> lock(smtx); // Совместный доступ
cout << shared_data << endl; // Совместное чтение
}
С++ 20 — jthread (автоматическое join):
#include <thread>
#include <mutex>
using namespace std;
mutex mtx;
void worker() {
lock_guard<mutex> guard(mtx);
// работа
}
int main() {
jthread t(worker); // Автоматически join в деструкторе
return 0;
}
Итоговые рекомендации
Мьютекс — это инструмент для исключительного доступа: только один поток может владеть ресурсом.
Семафор — это инструмент для ограничения доступа: N потоков могут работать с ресурсом одновременно.
Выбор между ними зависит от архитектуры:
- 1 потоку = мьютекс
- N потокам = семафор
- Читатели vs писатель = shared_mutex
Местерство в синхронизации потоков — это один из ключевых навыков для backend разработчика на C++, работающего с многопоточными приложениями.