Многопоточность в C++. Семафоры (semaphores)

Добавлено 31 декабря 2021 в 16:22

В 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
*/

Теги

C++ / CppC++20std::binary_semaphorestd::counting_semaphoreSTL / Standard Template Library / Стандартная библиотека шаблоновМногопоточностьСемафор / Semaphore

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.