Введение
QMutex и QSemaphore — два базовых примитива синхронизации в Qt, и начинающие разработчики иногда путают, когда применять каждый из них. Если упростить: мьютекс защищает критическую секцию от одновременного доступа более чем одного потока (бинарная блокировка), а семафор управляет доступом к ограниченному числу одинаковых ресурсов (счётный механизм). В статье разберём разницу через практические примеры.
Концепция
QMutex имеет два состояния — заблокирован/разблокирован — и предназначен для защиты данных, к которым в любой момент должен иметь доступ ровно один поток. QSemaphore хранит внутренний счётчик доступных «разрешений» (permits): acquire() уменьшает счётчик (блокируясь, если он равен нулю), release() увеличивает его. Это делает семафор удобным инструментом для ограничения количества одновременно работающих потоков с одним и тем же ресурсом (например, не более N одновременных подключений к базе данных) или для классической задачи «производитель-потребитель», где семафор отслеживает количество доступных элементов в буфере.
Важное отличие: семафор не привязан к конкретному потоку — release() может быть вызван из другого потока, чем тот, что делал acquire() (в отличие от рекурсивного мьютекса, который ожидает разблокировки тем же потоком, который заблокировал).
Пример кода
#include <QCoreApplication>
#include <QThread>
#include <QSemaphore>
#include <QMutex>
#include <QDebug>
#include <QVector>
// Ограничение числа одновременных "подключений" к ресурсу через семафор
class ConnectionPool
{
public:
explicit ConnectionPool(int maxConnections) : m_semaphore(maxConnections) {}
void useConnection(int workerId)
{
m_semaphore.acquire(); // ждём, если все слоты заняты
qDebug() << "Воркер" << workerId << "получил подключение";
QThread::msleep(500); // имитация работы с ресурсом
qDebug() << "Воркер" << workerId << "освобождает подключение";
m_semaphore.release();
}
private:
QSemaphore m_semaphore;
};
// Защита общего счётчика мьютексом
class SharedCounter
{
public:
void increment()
{
QMutexLocker locker(&m_mutex); // RAII-блокировка
m_value++;
}
int value() const
{
QMutexLocker locker(&m_mutex);
return m_value;
}
private:
mutable QMutex m_mutex;
int m_value = 0;
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
ConnectionPool pool(2); // максимум 2 одновременных подключения
SharedCounter counter;
QVector<QThread *> threads;
for (int i = 0; i < 5; ++i) {
auto *thread = QThread::create([&pool, &counter, i]() {
pool.useConnection(i);
counter.increment();
});
threads.append(thread);
thread->start();
}
for (auto *thread : threads) {
thread->wait();
delete thread;
}
qDebug() << "Итоговое значение счётчика:" << counter.value();
return 0;
}
Пояснения к коду
ConnectionPool использует QSemaphore, инициализированный значением 2 — это означает, что не более двух воркеров одновременно могут пройти acquire() без блокировки; остальные будут ждать, пока кто-то не вызовет release(). Это типичная реализация пула ограниченных ресурсов без явной очереди.
SharedCounter использует QMutex через QMutexLocker — RAII-обёртку, которая блокирует мьютекс в конструкторе и автоматически разблокирует в деструкторе, даже при выходе из функции через исключение. Это защищает простое инкрементирование общей переменной m_value от гонки данных при одновременном вызове increment() из нескольких потоков.
Подводные камни
- Использование мьютекса там, где нужен семафор, приводит к тому, что только один поток может работать с ресурсом за раз, даже если ресурс допускает параллельный доступ нескольким потокам (например, пул из нескольких подключений к БД) — это искусственно снижает параллелизм.
- Дисбаланс
acquire()/release(). Если поток приобрёл разрешение семафора, но завершился с ошибкой до вызоваrelease()(например, из-за необработанного исключения), счётчик семафора «протекает», и со временем все разрешения окажутся захвачены без возможности освобождения — нужно оборачиватьrelease()вtry/finally-подобную конструкцию или RAII-обёртку (аналогQMutexLocker, но для семафора нужно писать вручную или использоватьQSemaphoreReleaser). - Прямой вызов
lock()/unlock()мьютекса вместоQMutexLocker. Без RAII-обёртки забытыйunlock()при раннем выходе из функции (включая исключения) приводит к дедлоку для всех последующих обращений к этому мьютексу. - Рекурсивный захват обычного
QMutexтем же потоком приводит к дедлоку. Если требуется повторный захват мьютекса в рекурсивных вызовах одним и тем же потоком, нужно использоватьQRecursiveMutex, а не обычныйQMutex.