Многопоточность в C++. Семафоры (semaphores)
В C++20 в стандартной библиотеке появились семафоры.
Семафор (semaphore) – примитив синхронизации работы процессов и потоков, в основе которого лежит счётчик, над которым можно производить две атомарные операции: увеличение и уменьшение значения на единицу, при этом операция уменьшения для нулевого значения счётчика является блокирующей. Служит для построения более сложных механизмов синхронизации и используется для синхронизации параллельно работающих задач, для защиты передачи данных через разделяемую память, для защиты критических секций, а также для управления доступом к аппаратному обеспечению.
Семафоры могут быть двоичными и вычислительными. Вычислительные семафоры могут принимать целочисленные неотрицательные значения и используются для работы с ресурсами, количество которых ограничено, либо участвуют в синхронизации параллельно исполняемых задач. Двоичные семафоры могут принимать только значения 0 и 1 и используются для взаимного исключения одновременного нахождения двух или более процессов в своих критических секциях.
Мьютексные семафоры (мьютексы) являются упрощённой реализацией семафоров, аналогичной двоичным семафорам с тем отличием, что мьютексы должны отпускаться тем же потоком, который осуществляет их захват. Мьютексы наряду с двоичными семафорами используются в организации критических участков кода. В отличие от двоичных семафоров, начальное состояние мьютекса не может быть захваченным.
С помощью семафоров можно решать много разных задач синхронизации.
Проблемы, с которыми можно столкнуться при использовании семафоров.
Стандартная библиотека C++ предлагает к использованию вычислительные и двоичные семафоры, представленные классами std::counting_semaphore
и std::binary_semaphore
.
counting_semaphore–
это примитив синхронизации, который может управлять доступом к общему ресурсу. В отличие от мьютекса std::mutex
, counting_semaphore
допускает более одного параллельного доступа к одному и тому же ресурсу.
counting_semaphore
содержит внутренний счетчик, который инициализируется конструктором. Этот счетчик уменьшается при вызовах метода acquire()
и связанных с ним методов и увеличивается при вызовах метода release()
. Когда счетчик равен нулю, acquire()
блокирует поток до тех пор, пока счетчик не увеличится. Кроме того, для использования доступны методы:
try_acquire()
не блокирует поток, а возвращает вместо этогоfalse
. Подобноstd::condition_variable::wait()
, методtry_acquire()
может ошибочно возвращатьfalse
.try_acquire_for()
иtry_acquire_until()
блокируют до тех пор, пока счетчик не увеличится или не будет достигнут таймаут.
Семафоры нельзя копировать и перемещать.
В отличие от std::mutex
, counting_semaphore
не привязан к потокам выполнения – освобождение release()
и захват acquire()
семафора могут производиться в разных потоках (блокировка и освобождение мьютекса должны производиться одним и тем же потоком). Все операции над counting_semaphore
могут выполняться одновременно и без какой-либо связи с конкретными потоками выполнения.
Семафоры также часто используются для реализации уведомлений. При этом семафор инициализируется значением 0, и потоки, ожидающие события блокируются методом acquire()
, пока уведомляющий поток не вызовет release(n)
. В этом отношении семафоры можно рассматривать как альтернативу std::condition_variable
.
Методы acquire()
уменьшают значение счётчика семафора на 1. Методу release()
можно передать в качестве параметра значение, на которое должен быть увеличен счётчик, по умолчанию значение равно 1.
std::counting_semaphore<std::ptrdiff_t LeastMaxValue = /* implementation-defined */>
является шаблонным классом. В качестве параметра шаблона принимает значение, которое является нижней границей для максимально возможного значения счётчика. Фактический же максимум значений счётчика определяется реализацией и может быть больше, чем LeastMaxValue
.
binary_semaphore
– это просто псевдоним using binary_semaphore = std::counting_semaphore<1>;
.
Пример использования:
#include <iostream>
#include <thread>
#include <chrono>
#include <semaphore>
// глобальные экземпляры двоичных семафоров
// счетчик объектов установлен в ноль
// объекты в несигнальном состоянии
std::binary_semaphore
smphSignalMainToThread(0),
smphSignalThreadToMain(0);
void ThreadProc()
{
// ждем сигнала от main,
// пытаясь уменьшить значение семафора
smphSignalMainToThread.acquire();
// этот вызов блокируется до тех пор,
// пока счетчик семафора не увеличится в main
std::cout << "[thread] Got the signal\n"; // ответное сообщение
// ждем 3 секунды для имитации какой-то работы,
// выполняемой потоком
using namespace std::literals;
std::this_thread::sleep_for(3s);
std::cout << "[thread] Send the signal\n"; // сообщение
// сигнализируем обратно в main
smphSignalThreadToMain.release();
}
int main()
{
// создаем какой-то обрабатывающий поток
std::thread thrWorker(ThreadProc);
std::cout << "[main] Send the signal\n"; // сообщение
// сигнализируем рабочему потоку о начале работы,
// увеличивая значение счетчика семафора
smphSignalMainToThread.release();
// ждем, пока рабочий поток не выполнит свою работу,
// пытаясь уменьшить значение счетчика семафора
smphSignalThreadToMain.acquire();
std::cout << "[main] Got the signal\n"; // ответное сообщение
thrWorker.join();
}
/*
Вывод:
[main] Send the signal
[thread] Got the signal
[thread] Send the signal
[main] Got the signal
*/