Это не совсем вход ; Вы не запускаете функцию дважды в одном и том же потоке (или в разных потоках). Вы можете получить это с помощью рекурсии или передачи адреса текущей функции в виде функции-указателя функции обратного вызова в другую функцию. (И это не было бы небезопасно, потому что это было бы синхронно).
Это просто обычная гонка данных UB (Undefined Behavior) между обработчиком сигнала и основным потоком: только sig_atomic_t
гарантировано безопасное для этого . Другие могут работать, как в вашем случае, когда 8-байтовый объект может быть загружен или сохранен с одной инструкцией на x86-64, и компилятор выбирает этот asm. (Как показывает ответ @ icarus).
См. Программирование MCU - оптимизация C ++ O2 прерывается во время цикла - обработчик прерываний в одноядерном микроконтроллере - это то же самое, что и обработчик сигналов в однопоточной программе. В этом случае результат UB - то, что груз был поднят из петли.
Ваш тестовый случай разрыва действительно происходит из-за гонок данных UB, вероятно, был разработан / протестирован в 32-битном режиме или с более старым тупым компилятором, который загружал члены структуры отдельно.
В вашем случае компилятор может оптимизировать хранилища из бесконечного цикла, потому что ни одна свободная от UB программа никогда не сможет их наблюдать. data
не является _Atomic
илиvolatile
, и нет никаких других побочных эффектов в цикле. Так что никакой читатель не сможет синхронизироваться с этим писателем. Фактически это происходит, если вы компилируете с включенной оптимизацией ( Godbolt показывает пустой цикл внизу main). Я также изменил структуру на два long long
, и gcc использует одно movdqa
16-байтовое хранилище перед циклом. (Это не является гарантированным атомарным, но на практике это происходит практически на всех процессорах, при условии, что он выровнен, или на Intel просто не пересекает границу строки кэша. Почему целочисленное присваивание для естественно выровненной переменной атомарно на x86? )
Таким образом, компиляция с включенной оптимизацией также нарушит ваш тест и будет показывать вам одно и то же значение каждый раз. C не является переносимым языком ассемблера.
volatile struct two_int
также заставит компилятор не оптимизировать их, но не заставит его загружать / хранить всю структуру атомарно. (Однако это также не помешало бы сделать это.) Обратите внимание, что volatile
это не предотвращает гонку данных UB, но на практике этого достаточно для связи между потоками, и это было то, как люди создавали атомарную структуру вручную (наряду с встроенным ассемблером). до C11 / C ++ 11, для нормальной архитектуры ЦП. Они кэш-когерентный так volatile
это на практике в основном аналогична _Atomic
сmemory_order_relaxed
для чистой нагрузки и чистого-магазина, если они используются для типов достаточно узко , что компилятор будет использовать одну команду , так что вы не получите слезотечение. И конечноvolatile
не имеет никаких гарантий от стандарта ISO C против написания кода, который компилируется с использованием asm _Atomic
и mo_relaxed.
Если у вас есть функция , которая сделала global_var++;
по принципу int
или long long
запускать от основной и асинхронно из обработчика сигнала, который был бы способ использовать повторно entrancy для создания данных гонки UB.
В зависимости от того, как он скомпилирован (в место назначения памяти inc или add, или для разделения загрузки / inc / store), он будет атомарным или нет относительно обработчиков сигналов в том же потоке. См. Может ли num ++ быть атомарным для int num? подробнее об атомарности на x86 и в C ++. ( Атрибут C11 stdatomic.h
и _Atomic
обеспечивает эквивалентную функциональность для std::atomic<T>
шаблона C ++ 11 )
Прерывание или другое исключение не может произойти в середине инструкции, поэтому добавление к месту назначения памяти является атомарным. контекст переключается на одноядерный процессор. Только (совместимый с кэшем) DMA-писатель может «увеличить» шаг add [mem], 1
без lock
префикса на одноядерном процессоре. Нет никаких других ядер, на которых мог бы работать другой поток.
Так что это похоже на случай сигналов: обработчик сигнала запускается вместо обычного выполнения потока, обрабатывающего сигнал, поэтому он не может быть обработан в середине одной инструкции.