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

«В чем разница между семафором и мьютексом в контексте многопоточности C++?» — вопрос из категории Многопоточность, который задают на 25% собеседований C/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++ они используются по-разному.