← Назад к вопросам

В чём разница между мьютексом и семафором?

1.0 Junior🔥 181 комментариев
#Linux и операционные системы#Многопоточность и синхронизация#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Мьютекс vs Семафор

Определения

Мьютекс (Mutex) — взаимное исключение. Примитив синхронизации, который может находиться в двух состояниях: заблокирован (кем-то занят) или разблокирован (свободен). Только один поток может владеть мьютексом одновременно.

Семафор (Semaphore) — счётный примитив синхронизации. Имеет внутренний счётчик, который контролирует доступ к ресурсу. Несколько потоков могут одновременно работать с ресурсом, если счётчик > 0.

Основные различия

ХарактеристикаМьютексСемафор
ТипБинарный (2 состояния)Счётный (N состояний)
ВладелецДа (поток, заблокировавший)Нет (не привязан к потоку)
Одновременный доступ1 потокN потоков
МеханизмLock/UnlockWait/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;
}

Когда использовать

Используйте мьютекс:

  1. Для защиты критической секции кода
  2. Когда только один поток должен работать с ресурсом
  3. Для простой синхронизации
  4. Когда нужна гарантия, что поток, захративший мьютекс, его освободит

Используйте семафор:

  1. Для ограничения количества потоков, доступ к ресурсу (пул потоков)
  2. Когда N потоков могут работать с ресурсом одновременно
  3. Для сигнализации между потоками
  4. Для задач типа "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++, работающего с многопоточными приложениями.

В чём разница между мьютексом и семафором? | PrepBro