Isso não é realmente reentrância ; você não está executando uma função duas vezes no mesmo thread (ou em threads diferentes). Você pode obtê-lo por recursão ou passando o endereço da função atual como um argumento de retorno de chamada de função para outra função. (E não seria inseguro porque seria síncrono).
Este é apenas um simples UB (Undefined Behavior) entre um manipulador de sinal e o thread principal: somente isso sig_atomic_t
é garantido . Outros podem funcionar, como no seu caso, onde um objeto de 8 bytes pode ser carregado ou armazenado com uma instrução em x86-64, e o compilador escolhe esse asm. (Como mostra a resposta da @ icarus).
Veja Programação MCU - a otimização do C ++ O2 é interrompida durante o loop - um manipulador de interrupção em um microcontrolador de núcleo único é basicamente a mesma coisa que um manipulador de sinal em um único programa encadeado. Nesse caso, o resultado do UB é que uma carga foi içada de um loop.
Seu caso de teste de rasgo realmente acontecendo por causa da UB de corrida de dados provavelmente foi desenvolvido / testado no modo de 32 bits ou com um compilador mais antigo que carregou os membros da estrutura separadamente.
No seu caso, o compilador pode otimizar os armazenamentos a partir do loop infinito, porque nenhum programa livre de UB poderia observá-los. data
não é _Atomic
ouvolatile
e não há outros efeitos colaterais no loop. Portanto, não há como qualquer leitor sincronizar com este escritor. Isso de fato acontece se você compilar com a otimização ativada ( Godbolt mostra um loop vazio na parte inferior do main). Também mudei a estrutura para dois long long
e o gcc usa um único movdqa
armazenamento de 16 bytes antes do loop. (Isso não é garantido atômico, mas é na prática em quase todas as CPUs, supondo que esteja alinhado ou na Intel simplesmente não ultrapasse o limite da linha de cache. Por que a atribuição de número inteiro em uma variável atômica naturalmente alinhada no x86? )
Portanto, compilar com a otimização ativada também interromperia o teste e mostraria sempre o mesmo valor. C não é uma linguagem assembly portátil.
volatile struct two_int
também forçaria o compilador a não otimizá-lo, mas não forçaria o carregamento / armazenamento de toda a estrutura atomicamente. (Também não impediria que isso acontecesse.) Observe que volatile
isso não evita o UB de corrida de dados, mas, na prática, é suficiente para a comunicação entre threads e foi como as pessoas construíram átomos enrolados à mão (junto com asm inline) antes de C11 / C ++ 11, para arquiteturas normais de CPU. Eles são cache-coerente, volatile
é na prática, principalmente semelhante ao _Atomic
commemory_order_relaxed
por puro-carga e puro-store, se usado para tipos de estreitar o suficiente para que o compilador irá utilizar uma única instrução para que você não obter lacrimejamento. E clarovolatile
não possui nenhuma garantia do padrão ISO C vs. código de gravação que é compilado da mesma maneira usando _Atomic
e mo_relaxed.
Se você tivesse uma função executada global_var++;
em int
ou long long
executada a partir de main e assincronamente a partir de um manipulador de sinal, essa seria uma maneira de usar a reentrada para criar UB de corrida de dados.
Dependendo de como foi compilado (para um destino de memória incluído ou adicionado, ou para separar carga / inc / armazenamento), seria atômico ou não em relação aos manipuladores de sinal no mesmo encadeamento. Consulte Num ++ pode ser atômico para 'int num'? para saber mais sobre atomicidade em x86 e em C ++. (O C11 stdatomic.h
e o _Atomic
atributo fornecem funcionalidade equivalente ao modelo do C ++ 11 std::atomic<T>
)
Uma interrupção ou outra exceção não pode acontecer no meio de uma instrução, portanto, um add de destino de memória é wrt atômico. contexto alterna em uma CPU de núcleo único. Somente um gravador de DMA (coerente em cache) poderia "avançar" em um incremento de um add [mem], 1
sem lock
prefixo em uma CPU de núcleo único. Não há outros núcleos nos quais outro thread possa estar sendo executado.
Portanto, é semelhante ao caso dos sinais: um manipulador de sinal é executado em vez da execução normal do encadeamento que manipula o sinal, portanto, não pode ser manipulado no meio de uma instrução.