Вы используете объект типа sig_atomic_t
, доступ к которому осуществляется двумя потоками (с одним изменением).
Для модели памяти C ++ 11 это неопределенное поведение, и простое решение заключается в использовании std::atomic<T>
std::sig_atomic_t
и std::atomic<T>
находятся в разных лигах. В коде portable один не может быть заменен другим, и наоборот.
Единственное свойствочто обе доли - это атомарность (неделимые операции).Это означает, что операции над объектами этих типов не имеют (наблюдаемого) промежуточного состояния, но это так далеко, как сходство.
sig_atomic_t
не имеет свойств между потоками.Фактически, если объект этого типа доступен (изменен) более чем одним потоком (как в вашем примере кода), это технически неопределенное поведение (гонка данных);Поэтому свойства упорядочения памяти между потоками не определены.
для чего используется sig_atomic_t
?
Объект этого типа может использоваться в обработчике сигналов, но только если он объявлен volatile
.Атомарность и volatile
гарантируют 2 вещи:
- атомарность: обработчик сигнала может асинхронно сохранять значение для объекта, и любой, кто читает одну и ту же переменную (в том же потоке), может наблюдать только доили после значения.
- volatile: хранилище не может быть «оптимизировано» компилятором и поэтому видимо (в том же потоке) в (или после) точке, где сигнал прервал выполнение.
Например:
volatile sig_atomic_t quit {0};
void sig_handler(int signo) // called upon arrival of a signal
{
quit = 1; // store value
}
void do_work()
{
while (!quit) // load value
{
...
}
}
Хотя этот код является однопоточным, do_work
может быть прерван асинхронно с помощью сигнала, который запускает sig_handler
и атомно изменяет значение quit
,Без volatile
компилятор может «поднять» нагрузку из quit
из цикла while, делая невозможным для do_work
наблюдение изменения quit
, вызванного сигналом.
Почему std::atomic<T>
нельзя использовать в качестве замены std::sig_atomic_t
?
Вообще говоря, шаблон std::atomic<T>
- это другой тип, потому что он предназначен для одновременного доступа несколькихпотоков и обеспечивает межпотоковые гарантии заказа.Атомарность не всегда доступна на уровне ЦП (особенно для больших типов T
), и поэтому реализация может использовать внутреннюю блокировку для эмуляции атомарного поведения.Использование std::atomic<T>
блокировки для определенного типа T
доступно через функцию-член is_lock_free()
или константу класса is_always_lock_free
(C ++ 17).
Проблема с использованием этого типа вОбработчик сигнала заключается в том, что стандарт C ++ не гарантирует, что std::atomic<T>
не блокируется для любого типа T
.Только std::atomic_flag
имеет такую гарантию, но это другой тип.
Представьте приведенный выше код, где флаг quit
представляет собой std::atomic<int>
, который не блокируется.Есть вероятность, что когда do_work()
загружает значение, оно прерывается сигналом после получения блокировки, но перед ее снятием.Сигнал вызывает sig_handler()
, который теперь хочет сохранить значение в quit
, взяв ту же блокировку, которая уже была получена do_work
, упс.Это неопределенное поведение и, возможно, вызывает взаимоблокировку.
std::sig_atomic_t
не имеет этой проблемы, потому что он не использует блокировку.Все, что нужно, это тип, который неделим на уровне ЦП и на многих платформах, он может быть простым:
typedef int sig_atomic_t;
Суть в том, чтобы использовать volatile std::sig_atomic_t
для обработчиков сигналов в одном потокеи используйте std::atomic<T>
как тип без данных в многопоточном окружении.