Non è davvero un rientro ; non stai eseguendo una funzione due volte nello stesso thread (o in thread diversi). È possibile ottenerlo tramite la ricorsione o passando l'indirizzo della funzione corrente come un puntatore di funzione callback arg ad un'altra funzione. (E non sarebbe pericoloso perché sarebbe sincrono).
Questo è semplicemente un UB (Undefined Behaviour) di data race vaniglia tra un gestore di segnale e il thread principale: solo questo sig_atomic_t
è garantito per questo . Altri potrebbero funzionare, come nel tuo caso in cui un oggetto a 8 byte può essere caricato o archiviato con un'istruzione su x86-64, e il compilatore sembra scegliere quell'asm. (Come mostra la risposta di @ icarus).
Vedere la programmazione MCU - l'ottimizzazione O ++ di C ++ si interrompe durante il ciclo - un gestore di interrupt su un microcontrollore single-core è sostanzialmente la stessa cosa di un gestore di segnali in un singolo programma thread. In tal caso, il risultato dell'UB è che un carico è stato sollevato da un circuito.
Il tuo caso di prova di lacerazione che si verifica effettivamente a causa dell'UB-data-race è stato probabilmente sviluppato / testato in modalità 32-bit, o con un compilatore più vecchio più stupido che caricava gli elementi struct separatamente.
Nel tuo caso, il compilatore può ottimizzare gli archivi dal ciclo infinito perché nessun programma privo di UB potrebbe mai osservarli. data
non è _Atomic
ovolatile
e non ci sono altri effetti collaterali nel loop. Quindi non c'è modo che nessun lettore possa sincronizzarsi con questo scrittore. Questo infatti accade se si compila con l'ottimizzazione abilitata ( Godbolt mostra un loop vuoto nella parte inferiore di main). Ho anche cambiato la struttura in due long long
e gcc usa un singolo movdqa
archivio a 16 byte prima del ciclo. (Questo non è garantito atomico, ma è in pratica su quasi tutte le CPU, supponendo che sia allineato, o su Intel semplicemente non attraversa un limite di cache-line. Perché l'assegnazione di numeri interi su un atomico variabile naturalmente allineato su x86? )
Quindi la compilazione con l'ottimizzazione abilitata potrebbe anche interrompere il test e mostrare sempre lo stesso valore. C non è un linguaggio di assemblaggio portatile.
volatile struct two_int
forzerebbe anche il compilatore a non ottimizzarli, ma non lo forzerebbe a caricare / archiviare atomicamente l'intera struttura. (Non sarebbe smettere di farlo per entrambi, però.) Si noti che volatile
non senza evitare UB dati-gara, ma in pratica è sufficiente per la comunicazione inter-thread ed era come la gente costruito Atomics arrotolate a mano (con asm inline) prima di C11 / C ++ 11, per le normali architetture della CPU. Sono cache-coerente così volatile
è , in pratica, in gran parte simile a _Atomic
conmemory_order_relaxed
per puro carico e puro-store, se utilizzato per tipi Limita sufficiente che il compilatore utilizzerà una singola istruzione in modo da non ottiene strappo. E naturalmentevolatile
non ha alcuna garanzia dallo standard ISO C rispetto al codice di scrittura che viene compilato nello stesso modo in cui si usa _Atomic
e mo_relaxed.
Se avessi una funzione che ha funzionato global_var++;
su un int
o long long
che esegui da main e in modo asincrono da un gestore di segnale, sarebbe un modo per utilizzare la re-immissione per creare un UB data-race.
A seconda di come è stato compilato (in una destinazione di memoria inc o add, o per separare load / inc / store) sarebbe atomico o meno rispetto ai gestori di segnali nello stesso thread. Vedi num ++ può essere atomico per 'int num'? per ulteriori informazioni sull'atomicità su x86 e in C ++. (C11 stdatomic.h
e l' _Atomic
attributo forniscono funzionalità equivalenti al modello di C ++ 11 std::atomic<T>
)
Una interruzione o altra eccezione non può avvenire nel mezzo di un'istruzione, quindi un'aggiunta di destinazione di memoria è wrt atomica. il contesto commuta su una CPU single-core. Solo un masterizzatore DMA (coerente con la cache) può "incrementare" un incremento da a add [mem], 1
senza lock
prefisso su una CPU single-core. Non ci sono altri core su cui potrebbe essere in esecuzione un altro thread.
Quindi è simile al caso dei segnali: un gestore di segnale esegue invece la normale esecuzione del thread che gestisce il segnale, quindi non può essere gestito nel mezzo di un'istruzione.