#новичкам
Мьютексы довольно часто используют при разработке потокобезопасных модулей. Хоть их использование в высокопроизводительных приложениях нежелательно, они все равно являются базовым выбором для реализации наивной потокобезопасности. Более менее все про них знают.
А вот про семафоры - не все. В стандарт их добавили только в С++20 в виде std::counting_semaphore и std::binary_semaphore. Да и в принципе они не так часто используются.
Однако мьютексы и семафоры довольно похожи по внутренней реализации, хоть и довольно сильно отличаются по кейсам применения. Поэтому есть смысл их сравнить side-by-side, чтобы наглядно видеть все похожести и отличия.
Аналогия
Мьютекс - это дверь в очень маленькую туалетную комнату. Когда она свободна, любой может в нее войти. Любой, но только один. Как только кто-то вошел, все остальные начинают выстраиваться в очередь и ждать освобождения комнаты. А освободить комнату может только тот, кто в нее вошел.
Семафор - это турникет для автопарковки. У парковки есть определенное количество машин, которое на ней может разместиться. Больше не получится - места не будет, поэтому и турникет не пропустит. И машины опять выстраиваются в очередь. И только когда одна машина освободила место с парковки, одна новая может на нее заехать.
Для чего используется
Мьютекс - судя из названия mutual exclusion - взаимное исключение. Применяется, когда только один поток в один момент времени может получить доступ к разделяемому ресурсу.
Семафор же просто контролирует количество ресурсов и не дает уйти в минуса.
Низкоуровневое представление
Можно представить мьютекс как атомарный флаг, к которому прикручен механизм ожидания и очереди. Флаг можно выставить вверх и тогда попытка снова выставить его вверх карается блокировкой этого потока. Флаг можно опустить и тогда его может снова поднять любой желающий поток, либо тот, кто был первый в очереди на ожидание.
Семафор же - атомарный счетчик с таким же механизмом ожидания и очереди. Счетчик инициализируется каким-то числом и его можно инкрементировать и декрементировать. При попытке декремента нуля поток уходит в ожидание. Если какой-то другой поток снова накрутил единичку на семафоре - поток просыпается и делает таки свой декремент.
Владелец
У мьютекса есть владелец - тот, кто захватил замок, должен его отпустить. Если каким-то образом в коде это условие нарушается - сразу ub.
У семафора же нет никаких ограничений - любой поток может накручивать и скручивать счетчик.
Примеры
Мьютекс нужен для ограничения доступа потоков к критической секции. Например у вас есть какой-то кэш и вы хотите его потокобезопасно обновить:
std::mutex mtx;
std::map<std::string, int> cache;
void update_cache(const std::string& key, int value) {
std::lock_guard<std::mutex> lock(mtx);
cache[key] = value;
}
Это все конечно нужно обернуть в класс, но суть понятна и так.
Семафор же нужен для ограничения количество одновременно используемых ресурсов.
Например, ограниченная потокобезопасная очередь:
template<typename T, size_t N>
class BoundedQueue {
std::queue<T> queue_;
std::counting_semaphore<N> empty_slots_{N};
std::counting_semaphore<N> filled_slots_{0};
std::mutex mtx_;
public:
void push(T value) {
empty_slots_.acquire();
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(std::move(value));
}
filled_slots_.release();
}
T pop() {
filled_slots_.acquire();
T value;
{
std::lock_guard<std::mutex> lock(mtx_);
value = std::move(queue_.front());
queue_.pop();
}
empty_slots_.release();
return value;
}
};
Здесь семафоры нужны для того, чтобы в очередь не положили больше элементов, чем нужно, и не забрали больше, чем в очереди может потенциально быть. Обратите внимание на порядок инкремента и декремента.
Лайк, если понравилось. Да и если не понравилось, тоже ставьте.
Compare things. Stay cool.
#concurrency #cpp20 #cpp11