Eso no es realmente arrepentimiento ; no está ejecutando una función dos veces en el mismo hilo (o en hilos diferentes). Puede obtener eso a través de la recursión o pasando la dirección de la función actual como un argumento de puntero de función de devolución de llamada a otra función. (Y no sería inseguro porque sería sincrónico).
Esto es simplemente UB (comportamiento indefinido) de vainilla de datos entre un controlador de señal y el hilo principal: solo sig_atomic_t
se garantiza su seguridad . Puede que otros funcionen, como en el caso en que un objeto de 8 bytes se puede cargar o almacenar con una instrucción en x86-64, y el compilador elige ese asm. (Como muestra la respuesta de @ icarus).
Consulte la programación de MCU: la optimización de C ++ O2 se rompe durante el bucle : un controlador de interrupción en un microcontrolador de núcleo único es básicamente lo mismo que un controlador de señal en un programa de subproceso único. En ese caso, el resultado del UB es que una carga se levantó de un bucle.
Su caso de prueba de desgarro en realidad debido a la carrera de datos UB probablemente se desarrolló / probó en modo de 32 bits, o con un compilador más tonto que cargó los miembros de la estructura por separado.
En su caso, el compilador puede optimizar las tiendas desde el bucle infinito porque ningún programa sin UB podría observarlas. data
no es _Atomic
ovolatile
, y no hay otros efectos secundarios en el ciclo. Así que no hay forma de que ningún lector pueda sincronizarse con este escritor. De hecho, esto sucede si compila con la optimización habilitada ( Godbolt muestra un bucle vacío en la parte inferior de main). También cambié la estructura a dos long long
, y gcc usa una sola movdqa
tienda de 16 bytes antes del ciclo. (Esto no está garantizado atómico, pero en la práctica en casi todas las CPU, suponiendo que esté alineado, o en Intel simplemente no cruza un límite de línea de caché. ¿Por qué la asignación de enteros en una variable atómica alineada naturalmente en x86? )
Por lo tanto, compilar con la optimización habilitada también rompería su prueba y le mostraría el mismo valor cada vez. C no es un lenguaje ensamblador portátil.
volatile struct two_int
También forzaría al compilador a no optimizarlos, pero no forzaría a cargar / almacenar la estructura completa atómicamente. (No sería dejar que lo hagan bien, sin embargo.) Tenga en cuenta que volatile
no no evitar UB-raza de datos, pero en la práctica es suficiente para la comunicación entre hilos y fue cómo las personas construyen atómica enrollados a mano (junto con asm en línea) antes de C11 / C ++ 11, para arquitecturas de CPU normales. Son caché coherente por lo que volatile
es en la práctica, sobre todo similar a _Atomic
lamemory_order_relaxed
de pura carga y puro de la tienda, si se utiliza para Limitar suficiente como para que el compilador se utilice una sola instrucción para que no se consigue rasgado. Y por supuestovolatile
no tiene ninguna garantía del estándar ISO C vs. escritura de código que se compila al mismo asm usando _Atomic
y mo_relaxed.
Si tuviera una función que hiciera global_var++;
en una int
o long long
que se ejecute desde main y de forma asincrónica desde un controlador de señal, esa sería una forma de usar el reencuentro para crear UB de carrera de datos.
Dependiendo de cómo se compiló (a un destino de memoria inc o add, o para separar load / inc / store) sería atómico o no con respecto a los manejadores de señal en el mismo hilo. Consulte ¿Puede num ++ ser atómico para 'int num'? para más información sobre atomicidad en x86 y en C ++. (C11 stdatomic.h
y el _Atomic
atributo proporcionan una funcionalidad equivalente a la std::atomic<T>
plantilla de C ++ 11 )
Una interrupción u otra excepción no puede suceder en medio de una instrucción, por lo que una adición de destino de memoria es atómica wrt. El contexto activa una CPU de un solo núcleo. Solo un escritor de DMA (coherente de caché) podría "pisar" un incremento desde un add [mem], 1
sin lock
prefijo en una CPU de un solo núcleo. No hay otros núcleos en los que se pueda ejecutar otro subproceso.
Por lo tanto, es similar al caso de las señales: se ejecuta un controlador de señales en lugar de la ejecución normal del subproceso que maneja la señal, por lo que no se puede manejar en medio de una instrucción.