To nie jest tak naprawdę ponowne wejście ; nie uruchamiasz funkcji dwukrotnie w tym samym wątku (lub w różnych wątkach). Możesz to uzyskać poprzez rekurencję lub przekazanie adresu bieżącej funkcji jako argument funkcji zwrotnej arg do innej funkcji. (I nie byłoby to niebezpieczne, ponieważ byłoby synchroniczne).
To jest po prostu waniliowy wyścig danych UB (niezdefiniowane zachowanie) między procedurą obsługi sygnału a głównym wątkiem: tylko sig_atomic_t jest bezpieczny . Inne mogą działać, na przykład w twoim przypadku, gdy 8-bajtowy obiekt można załadować lub zapisać za pomocą jednej instrukcji na x86-64, a kompilator wybiera taki asm. (Jak pokazuje odpowiedź @ icarus).
Zobacz programowanie MCU - optymalizacja C ++ O2 zrywa podczas pętli - procedura obsługi przerwań na mikrokontrolerze z jednym rdzeniem jest w zasadzie taka sama jak procedura obsługi sygnałów w programie z jednym wątkiem. W takim przypadku wynikiem UB jest to, że ładunek został wyciągnięty z pętli.
Twój przypadek testowy zerwania faktycznie zachodzi z powodu wyścigu danych UB został prawdopodobnie opracowany / przetestowany w trybie 32-bitowym lub ze starszym głupszym kompilatorem, który ładował osobno elementy struktury.
W twoim przypadku kompilator może zoptymalizować zapasy z nieskończonej pętli, ponieważ żaden program bez UB nigdy ich nie zaobserwuje. datanie ma _Atomiclubvolatile , i nie ma innych efektów ubocznych w pętli. Więc nie ma mowy, aby jakikolwiek czytnik mógł zsynchronizować się z tym pisarzem. Dzieje się tak, jeśli kompilujesz z włączoną optymalizacją ( Godbolt pokazuje pustą pętlę u dołu głównego). Zmieniłem również struct na dwa long long, a gcc używa pojedynczego movdqa16-bajtowego magazynu przed zapętleniem. (Nie jest to gwarantowane atomowo, ale w praktyce działa na prawie wszystkich procesorach, zakładając, że jest wyrównane, lub na Intelie po prostu nie przekracza granicy linii pamięci podręcznej. Dlaczego przypisanie liczb całkowitych naturalnie wyrównanej zmiennej atomowej na x86? )
Zatem kompilacja z włączoną optymalizacją również przerwałaby test i za każdym razem pokazywałaby tę samą wartość. C nie jest przenośnym językiem asemblera.
volatile struct two_intzmusiłoby również kompilator do nieoptymalizowania ich, ale nie zmusiłoby go do załadowania / przechowywania całej struktury atomowo. (Nie byłoby powstrzymać go od tego czy, choć.) Zauważ, że volatilenie nie uniknąć danych wyścigu UB, ale w praktyce jest to wystarczające dla komunikacji między gwintem i było to, jak ludzie budowane ręcznie walcowane ATOMiCS (wraz z inline ASM) przed C11 / C ++ 11, dla normalnych architektur CPU. Są cache-spójny tak volatilejest w praktyce przeważnie podobny do _Atomiczmemory_order_relaxed czystej obciążenia i czystej-sklepu, jeśli są stosowane dla typów zawęzić tyle że kompilator użyje pojedynczą instrukcję, aby nie dostać łzawienie. I oczywiścievolatilenie ma żadnych gwarancji ze standardu ISO C w porównaniu do pisania kodu, który kompiluje się do tego samego asm przy użyciu _Atomici mo_relaxed.
Jeśli miałeś funkcję, która działała global_var++;na intlub long long, że biegniesz z głównego i asynchronicznie z procedury obsługi sygnału, byłby to sposób na użycie ponownego wejścia do utworzenia UB wyścigu danych.
W zależności od sposobu kompilacji (do miejsca docelowego pamięci inc lub add, lub do oddzielenia load / inc / store) byłoby atomowe lub nie w odniesieniu do procedur obsługi sygnałów w tym samym wątku. Zobacz Can num ++ be atomic dla 'int num'? więcej informacji o atomowości na x86 i w C ++. (C11 stdatomic.hi _Atomicatrybut zapewniają funkcjonalność równoważną std::atomic<T>szablonowi C ++ 11 )
Przerwanie lub inny wyjątek nie może się zdarzyć w środku instrukcji, więc dodanie do miejsca docelowego pamięci jest niepodzielne. kontekst włącza jednordzeniowy procesor. Jedynie (spójny z pamięcią podręczną) moduł zapisujący DMA mógł „nadepnąć” na przyrost z prefiksu add [mem], 1bez lockprefiksu na jednordzeniowy procesor. Nie ma żadnych innych rdzeni, na których mógłby działać inny wątek.
Jest to więc podobne do przypadku sygnałów: procedura obsługi sygnału działa zamiast normalnego wykonania wątku obsługującego sygnał, więc nie można go obsłużyć w środku jednej instrukcji.