Вы используете объект типа sig_atomic_t
, к которому обращаются два потока (с одним изменением).
Согласно модели памяти C ++ 11, это поведение undefined, и простое решение - использовать std::atomic<T>
std::sig_atomic_t
и std::atomic<T>
находятся в разных лигах. В переносном коде нельзя заменить одно другим, и наоборот.
Единственное общее свойство - атомарность (неделимые операции). Это означает, что операции с объектами этих типов не имеют (наблюдаемого) промежуточного состояния, но это пока что сходство.
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>
как тип без гонки данных в многопоточной среде.
person
LWimsey
schedule
14.06.2019
std::atomic_signal_fence
, я предполагаю, что нет никаких гарантий заказа для все, что связано с «сигналом». - person Mgetz   schedule 14.06.2019volatile
означает дать мне семантику базового процессора и системы памяти. Таким образом, Q по своей сути подразумевает зависимость (теоретически). На практике да, то же самое. - person curiousguy   schedule 15.06.2019