Estaba estudiando el reencuentro en la programación. En este sitio de IBM (realmente bueno). He fundado un código, copiado a continuación. Es el primer código que viene rodando por el sitio web.
El código intenta mostrar los problemas relacionados con el acceso compartido a la variable en un desarrollo no lineal de un programa de texto (asincronía) al imprimir dos valores que cambian constantemente en un "contexto peligroso".
#include <signal.h>
#include <stdio.h>
struct two_int { int a, b; } data;
void signal_handler(int signum){
printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){
static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1){
data = zeros;
data = ones;
}
}
Los problemas aparecieron cuando intenté ejecutar el código (o mejor, no apareció). Estaba usando gcc versión 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) en la configuración predeterminada. No se produce la salida equivocada. ¡La frecuencia para obtener valores de par "incorrectos" es 0!
¿Qué está pasando después de todo? ¿Por qué no hay ningún problema en volver a estar fascinado usando variables globales estáticas?
Respuestas:
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 doslong long
, y gcc usa una solamovdqa
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 quevolatile
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 quevolatile
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 unaint
olong 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 lastd::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
sinlock
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.
fuente
Mirando el explorador del compilador godbolt (después de agregar los que faltan
#include <unistd.h>
), uno ve que para casi cualquier compilador x86_64 el código generado usa movimientos QWORD para cargarones
yzeros
en una sola instrucción.El sitio de IBM dice
On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.
qué podría haber sido cierto para los cpus típicos en 2005, pero como muestra el código, ahora no es cierto. Cambiar la estructura para tener dos longs en lugar de dos ints mostraría el problema.Anteriormente escribí que esto era "atómico", que era vago. El programa solo se ejecuta en una sola CPU. Cada instrucción se completará desde el punto de vista de esta CPU (suponiendo que no haya nada más que altere la memoria, como dma).
Entonces, en el
C
nivel no está definido que el compilador elegirá una sola instrucción para escribir la estructura, por lo que puede ocurrir la corrupción mencionada en el documento de IBM. Los compiladores modernos que apuntan a los cpus actuales utilizan una sola instrucción. Una sola instrucción es lo suficientemente buena como para evitar la corrupción de un solo programa de subprocesos.fuente
int
along long
y compile a 32 bits. La lección es que nunca se sabe si / cuándo se romperá.long long
Todavía compila a una instrucción para x86-64: 16 bytesmovdqa
. A menos que desactive la optimización, como en su enlace Godbolt. (El valor predeterminado de GCC es el-O0
modo de depuración, que está lleno de ruido de almacenamiento / recarga y, por lo general, no es interesante de ver.)