В чем разница между семафором и мьютексом в контексте многопоточности C++?

Ответ

Мьютекс и семафор — это примитивы синхронизации с разной семантикой.

Мьютекс (mutex, mutual exclusion):

  • Назначение: Обеспечить взаимное исключение для доступа к общему ресурсу. В один момент времени мьютексом может владеть только один поток.
  • Владение: Мьютекс должен быть освобожден тем же потоком, который его захватил. Это делает его идеальным для защиты критических секций.
  • В C++: std::mutex, std::timed_mutex, std::recursive_mutex. Используется вместе с std::lock_guard или std::unique_lock для автоматического управления временем жизни.
#include <mutex>
#include <thread>
#include <iostream>

std::mutex g_mutex;
int shared_data = 0;

void safe_increment() {
    std::lock_guard<std::mutex> lock(g_mutex); // Захват при создании, освобождение при разрушении
    ++shared_data;
    std::cout << "Thread " << std::this_thread::get_id() 
              << ": shared_data = " << shared_data << 'n';
}

Семафор (semaphore):

  • Назначение: Ограничить доступ к ресурсу (или пулу ресурсов) N потокам одновременно. Семафор управляет внутренним счетчиком.
  • Владение: Поток, который уменьшил счетчик (acquire), не обязан быть тем же, который его увеличивает (release). Это позволяет реализовать шаблоны типа "производитель-потребитель".
  • В C++: std::counting_semaphore (C++20). Имитирует доступ к ограниченному количеству ресурсов.
#include <semaphore>
#include <thread>
#include <iostream>
#include <vector>

// Допустим, у нас есть пул из 3 подключений к БД
std::counting_semaphore<10> db_connection_semaphore(3); // Максимум 3, изначально 3 свободны

void query_database(int query_id) {
    db_connection_semaphore.acquire(); // Ждет, если свободных подключений нет
    // Симуляция работы с БД
    std::cout << "Query " << query_id << " started...n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "Query " << query_id << " finished.n";
    db_connection_semaphore.release(); // Освобождает подключение
}

Ключевые различия:

  1. Счетчик: Мьютекс по сути бинарный (0 или 1). Семафор имеет счетчик (0..N).
  2. Владение: У мьютекса есть понятие владельца, у семафора — нет.
  3. Использование: Мьютекс защищает данные от одновременного доступа. Семафор управляет доступом к ресурсам (например, к пулу потоков, подключениям).

Аналог: Мьютекс — это частный случай семафора с максимальным счетчиком, равным 1. Однако из-за семантики владения в C++ они используются по-разному.

Ответ 18+ 🔞

А, ну вот, опять про эти мьютексы и семафоры. Слушай, давай разложу по полочкам, а то народ путается, как будто в первый раз слышит.

Представь себе, блядь, туалет в офисе. Один унитаз. Мьютекс — это как защёлка на двери изнутри. Зашёл, щёлк — заперся. Пока ты там не сделаешь свои дела и не откроешь, никто другой не влезет. Владение строгое: кто закрыл, тот и открывает. Идеально, чтобы защитить свою жопу, то есть критическую секцию кода, от посторонних глаз. В С++ это std::mutex, а lock_guard — это такой автомат, который щёлкает за тебя: вошёл — запер, вышел — сам отпер. Удобно, ёпта.

std::mutex g_mutex;
int shared_data = 0;

void safe_increment() {
    std::lock_guard<std::mutex> lock(g_mutex); // Вошёл в кабинку — щёлк!
    ++shared_data;
}

А теперь семафор — это уже не один унитаз, а, допустим, три кабинки в общем туалете. Есть счётчик свободных мест. std::counting_semaphore<10> — это как табличка «свободно: 3». Зашёл ты (acquire) — счётчик уменьшился. Может, за тобой зайдёт твой кореш и тоже займёт кабинку. А выйдет (release) сначала он, а потом ты. Или наоборот. Владения нет, главное — чтобы счётчик не ушёл в минус. Это ж для управления пулом ресурсов, типа подключений к базе. Овердохуища потоков могут работать, но одновременно — только N штук.

// Пусть будет 3 подключения к базе, больше не выдержит
std::counting_semaphore<10> db_semaphore(3);

void query_database(int query_id) {
    db_semaphore.acquire(); // Ждём, пока не освободится хоть одна кабинка-коннект
    // ... делаем запрос ...
    db_semaphore.release(); // Отпустили, пусть другие пользуются
}

Так в чём, блядь, разница-то, спросишь ты? А разница пиздец принципиальная.

  1. Счётчик. Мьютекс — он как девственница, бинарный: либо занят (1), либо свободен (0). Семафор — шлюха опытная, у него счётчик от 0 до N. Может обслуживать несколько потоков сразу, если N > 1.
  2. Владение. Мьютекс — хитрая жопа, требует, чтобы отпускал тот же, кто и взял. Семафору похуй: один потокищщщ взял, другой — отпустил. Как в том анекдоте про «мужик входит в бар...». Это для паттернов типа «производитель-потребитель» — самое то.
  3. Для чего. Мьютекс — чтобы данные не ебнулись, когда к ним одновременно лезут. Семафор — чтобы доступ к ресурсам (тем же потокам, коннектам, принтерам) ограничить.

Аналог простой: мьютекс — это вырожденный случай семафора, у которого счётчик максимум 1. Но из-за этой своей привередливой семантики владения в С++ их используют по-разному. Не путай, а то будет тебе хиросима в коде, чувак.