std :: atomic ‹int› memory_order_relaxed VS volatile sig_atomic_t в многопоточной программе

volatile sig_atomic_t дает ли какие-либо гарантии порядка памяти? Например. если мне нужно просто загрузить / сохранить целое число, можно ли его использовать?

Например. здесь:

volatile sig_atomic_t x = 0;
...
void f() {
  std::thread t([&] {x = 1;});
  while(x != 1) {/*waiting...*/}
  //done!
}

это правильный код? Есть условия, что он может не работать?

Примечание: это упрощенный пример, т.е. я не ищу лучшего решения для данного фрагмента кода. Я просто хочу понять, какого поведения можно ожидать от volatile sig_atomic_t в многопоточной программе в соответствии со стандартом C ++. Или, если это так, понять, почему поведение не определено.

Я нашел следующее утверждение здесь:

Тип библиотеки sig_atomic_t не обеспечивает межпотоковую синхронизацию или упорядочение памяти, только атомарность.

И если я сравню это с этим определением, здесь:

memory_order_relaxed: расслабленная операция: нет ограничений синхронизации или упорядочения, налагаемых на другие операции чтения или записи, гарантируется только атомарность этой операции

Разве это не то же самое? Что именно здесь означает атомарность? volatile делает здесь что-нибудь полезное? Какая разница между «не обеспечивает синхронизацию или упорядочение памяти» и «без ограничений синхронизации или упорядочивания»?


person kan    schedule 14.06.2019    source источник
comment
Похоже, что единственная гарантия, которую он дает, - это то, что это безопасно для сигналов, я бы не стал ' t использовать его для многопоточности. Основываясь на документации для std::atomic_signal_fence, я предполагаю, что нет никаких гарантий заказа для все, что связано с «сигналом».   -  person Mgetz    schedule 14.06.2019
comment
Проверьте это: std :: condition_variable может лучше соответствовать вашим потребностям ( не уверен, так как вы не предоставили хорошее описание).   -  person Marek R    schedule 14.06.2019
comment
volatile означает дать мне семантику базового процессора и системы памяти. Таким образом, Q по своей сути подразумевает зависимость (теоретически). На практике да, то же самое.   -  person curiousguy    schedule 15.06.2019


Ответы (1)


Вы используете объект типа 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
comment
Короче говоря, volatile sig_atomic_t не гарантирует видимости изменения вне обработки сигнала. :-) - person Andrew Henle; 14.06.2019
comment
Ok. Вы говорите, что sig_atomic ... может просто не стать видимым для загрузки ..., что очень близко к memory_order_relaxed: Расслабленная операция: нет ограничений синхронизации или упорядочения, налагаемых на другие чтения или пишет, гарантируется атомарность только этой операции. Как по мне, это звучит точно так же (и да, допустим, это lock_free или atomic_flag, а не какой-нибудь atomic<>). Итак, могу ли я сделать вывод, что поведение такое же? Или мне не следует делать такие предположения и поведение не определено? - person kan; 14.06.2019
comment
@kan ... может не стать видимым для загрузки ... - это просто пример того, как может проявляться «неопределенное поведение», но я удалю эту фразу, потому что она сбивает с толку. Неопределенное поведение - это все, что нужно знать. - person LWimsey; 14.06.2019
comment
@AndrewHenle atomic имеет очень плохие гарантии видимости. И плохая семантика. Плохо все. - person curiousguy; 15.06.2019
comment
Извините за глупый вопрос, но почему sig_handler ... взятие той же блокировки, которая уже была получена do_work, является ошибкой и UB? Почему не sig_handler просто подождать, пока do_work снимет блокировку? - person Evg; 15.06.2019
comment
@Evg Не глупый вопрос .. Причина в том, что все происходит в одном потоке выполнения .. Есть только один порядок, в котором выполняются инструкции. Таким образом, если sig_handler ожидает получения блокировки, которая уже удерживается do_work, ни одна из функций не может продвигаться вперед, и весь поток находится в тупике. С несколькими потоками этой проблемы не существует. - person LWimsey; 16.06.2019
comment
@LWimsey, понял. Спасибо! - person Evg; 16.06.2019