Це насправді не обов'язок ; ви не виконуєте функції двічі в одному потоці (або в різних потоках). Ви можете отримати це за допомогою рекурсії або передачі адреси поточної функції як аргумент-покажчик функції зворотного виклику на іншу функцію. (І це не було б небезпечно, оскільки це було б синхронно).
Це просто звичайна UB-перегонка для передачі даних ванілі (Undefined Behavior) між обробником сигналу та головним потоком: лише sig_atomic_t
гарантована безпека для цього . Інші можуть траплятися, як у вашому випадку, коли 8-байтний об’єкт можна завантажувати або зберігати з однією інструкцією на x86-64, а компілятор вибирає цю зону. (Як показує відповідь @ icarus).
Див. Програмування MCU - оптимізація C ++ O2 перервана під час циклу - обробник переривання на одноядерному мікроконтролері - це те саме, що обробник сигналу в одній потоковій програмі. У такому випадку результат UB полягає в тому, що навантаження піднімається з петлі.
Ваш тестовий випадок, коли насправді відбувається розрив через UB-перегони даних, був, ймовірно, розроблений / протестований у 32-бітному режимі або зі старшим компілятором dumber, який завантажував членів структури окремо.
У вашому випадку компілятор може оптимізувати магазини з нескінченного циклу, оскільки жодна програма без UB не могла їх спостерігати. data
немає _Atomic
абоvolatile
, і немає інших побічних ефектів у циклі. Тому жоден читач не міг би синхронізуватися з цим автором. Це насправді відбувається, якщо компілювати з увімкненою оптимізацією ( Godbolt показує порожній цикл у нижній частині основного). Я також змінив структуру на дві long long
, і gcc використовує єдиний movdqa
16-байтовий сховище перед циклом. (Це не є гарантованим атомним, але це на практиці майже на всіх процесорах, якщо припустити, що він вирівняний, або в Intel просто не перетинає межу кеш-лінії. Чому присвоєння цілого числа природно вирівняній атомній змінній на x86? )
Таким чином, компіляція з увімкненою оптимізацією також порушить ваш тест і показуватиме вам те саме значення кожного разу. C - це не портативна мова складання.
volatile struct two_int
Також змушує компілятор не оптимізувати їх, але не змусить його завантажувати / зберігати всю структуру атомним шляхом. (Це також не завадило б це зробити.) Зауважте, що volatile
це не дозволяє уникнути UB-перегонів з передачею даних, але на практиці це достатньо для зв'язку між потоками і те, як люди будували атоміку вручну (разом з inline asm) до C11 / C ++ 11, для звичайних архітектур процесора. Вони кеш-когерентний так volatile
це на практиці в основному аналогічна _Atomic
зmemory_order_relaxed
для чистої навантаження і чистого-магазину, якщо вони використовуються для типів досить вузько , що компілятор буде використовувати одну команду , так що ви не отримаєте сльозотеча. І звичайноvolatile
не має жодних гарантій від стандарту ISO C проти коду написання, який компілюється в один _Atomic
і той самий ASM, використовуючи та mo_relaxed.
Якщо у вас була функція, яка виконується global_var++;
на int
або, long long
що ви запускаєтесь від головного та асинхронно від обробника сигналу, це був би спосіб використовувати повторне вступ для створення UB-перегону даних.
Залежно від способу компіляції (до місця призначення пам'яті inc або add, або для розділення завантаження / inc / store), це буде атомним чи ні щодо відносно оброблювачів сигналів в одному потоці. Див. Чи може num ++ бути атомним для 'int num'? Докладніше про атомність на x86 та на C ++. (C11 stdatomic.h
та _Atomic
атрибут забезпечує еквівалентну функціональність std::atomic<T>
шаблону C ++ 11 )
Переривання або інший виняток не може статися в середині інструкції, тому додавання призначення пам'яті є атомним wrt. контекст вмикає одноядерний процесор. Лише (кешований кеш-пам'ять) DMA-письменник міг "наступити" на приріст від add [mem], 1
без lock
префікса одноядерного процесора. Немає інших ядер, на яких могла б працювати інша нитка.
Так це схоже на випадок сигналів: обробник сигналу працює замість нормального виконання потоку, що обробляє сигнал, тому його не можна обробляти посередині однієї інструкції.