Escribí un simple programa multiproceso de la siguiente manera:
static bool finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Se comporta normalmente en modo de depuración en Visual Studio o -O0
en gc c e imprime el resultado después de 1
segundos. Pero se atascó y no imprime nada en modo Release o -O1 -O2 -O3
.
c++
multithreading
thread-safety
data-race
sz ppeter
fuente
fuente
Respuestas:
Dos hilos, que acceden a una variable no atómica, no protegida, son UB. Esto concierne
finished
. Podrías hacerfinished
de tipostd::atomic<bool>
para arreglar esto.Mi solución:
Salida:
Demo en vivo en coliru
Alguien puede pensar 'Es un
bool
- probablemente un poco. ¿Cómo puede ser esto no atómico? (Lo hice cuando comencé a usar subprocesos múltiples).Pero tenga en cuenta que la falta de rasgaduras no es lo único que
std::atomic
le da. También hace que el acceso simultáneo de lectura + escritura desde múltiples hilos esté bien definido, evitando que el compilador suponga que releer la variable siempre verá el mismo valor.Hacer un
bool
descuidado, no atómico puede causar problemas adicionales:atomic<bool>
conmemory_order_relaxed
almacenamiento / carga, pero dondevolatile
no funcionaría. volátil para esto sería UB, a pesar de que funciona en la práctica en implementaciones reales de C ++).Para evitar que esto suceda, se debe indicar explícitamente al compilador que no lo haga.
Estoy un poco sorprendido por la evolución de la discusión sobre la posible relación de
volatile
este tema. Por lo tanto, me gustaría gastar mis dos centavos:fuente
func()
Eché un vistazo y pensé "Podría optimizar eso" El optimizador no se preocupa por los hilos en absoluto, y detectará el bucle infinito, y felizmente lo convertirá en un "tiempo (Verdadero)" Si miramos a Dios. .org / z / Tl44iN podemos ver esto. Si está terminadoTrue
, vuelve. Si no, entra en un salto incondicional a sí mismo (un bucle infinito) en la etiqueta.L5
volatile
en C ++ 11 porque puedes obtener un asm idéntico conatomic<T>
ystd::memory_order_relaxed
. Sin embargo, funciona en hardware real: los cachés son coherentes, por lo que una instrucción de carga no puede seguir leyendo un valor obsoleto una vez que una tienda en otro núcleo se compromete a almacenar en caché allí. (MESI)volatile
sigue siendo UB. Realmente nunca debe suponer algo que es definitivamente y claramente UB es seguro solo porque no puede pensar en una forma en que podría salir mal y funcionó cuando lo probó. Eso ha quemado a la gente una y otra vez.finished
con unstd::mutex
trabajo (sinvolatile
oatomic
). De hecho, puede reemplazar todos los elementos atómicos con un valor "simple" + esquema mutex; aún funcionaría y sería más lento.atomic<T>
se le permite usar un mutex interno; soloatomic_flag
se garantiza sin bloqueo.La respuesta de Scheff describe cómo arreglar su código. Pensé que agregaría un poco de información sobre lo que realmente está sucediendo en este caso.
Compilé tu código en godbolt usando el nivel de optimización 1 (
-O1
). Su función se compila así:¿Entonces, Que esta pasando aquí? Primero, tenemos una comparación:
cmp BYTE PTR finished[rip], 0
esto verifica sifinished
es falso o no.Si es no falsa (también conocido como verdadera) debemos salir del bucle en la primera ejecución. Esto logra
jne .L4
que j umps cuando n ot e qual a la etiqueta.L4
cuando el valor dei
(0
) se almacena en un registro para su uso posterior y se devuelve la función.Si se Sin embargo, es falso, pasamos a
Este es un salto incondicional, para etiquetar
.L5
que resulta ser el comando de salto en sí.En otras palabras, el hilo se coloca en un bucle ocupado infinito.
Entonces, ¿por qué ha sucedido esto?
En lo que respecta al optimizador, los hilos están fuera de su alcance. Se supone que otros hilos no leen o escriben variables simultáneamente (porque eso sería una carrera de datos UB). Debe decirle que no puede optimizar los accesos de distancia. Aquí es donde entra la respuesta de Scheff. No me molestaré en repetirlo.
Debido a que no se le dice al optimizador que la
finished
variable puede cambiar potencialmente durante la ejecución de la función, ve quefinished
la función en sí no la modifica y asume que es constante.El código optimizado proporciona las dos rutas de código que resultarán de ingresar a la función con un valor bool constante; o ejecuta el bucle infinitamente, o el bucle nunca se ejecuta.
en
-O0
el compilador (como se esperaba) no optimiza el cuerpo del bucle y la comparación:por lo tanto, la función, cuando no se optimiza, funciona, la falta de atomicidad aquí no suele ser un problema, porque el código y el tipo de datos son simples. Probablemente lo peor con lo que podríamos encontrarnos aquí es un valor de
i
uno a lo que debería ser.Un sistema más complejo con estructuras de datos es mucho más probable que produzca datos corruptos o una ejecución incorrecta.
fuente
atomic
variables en el código que no escribe esas variables. por ejemploif (cond) foo=1;
, no se puede transformar en asm,foo = cond ? 1 : foo;
porque esa carga + almacenamiento (no un RMW atómico) podría pisar una escritura desde otro hilo. Los compiladores ya estaban evitando cosas así porque querían ser útiles para escribir programas de subprocesos múltiples, pero C ++ 11 hizo oficial que los compiladores no debían romper el código donde escriben 2 hilosa[1]
ya[2]
En aras de la integridad en la curva de aprendizaje; Debe evitar el uso de variables globales. Hiciste un buen trabajo al hacerlo estático, por lo que será local para la unidad de traducción.
Aquí hay un ejemplo:
Live on wandbox
fuente
finished
comostatic
dentro del bloque de funciones. Todavía se inicializará solo una vez, y si se inicializa a una constante, esto no requiere bloqueo.finished
también podrían usarstd::memory_order_relaxed
carga y tiendas más baratas ; no se requiere ordenar wrt. otras variables en cualquier hilo. Sinstatic
embargo, no estoy seguro de que la sugerencia de @ Davislor tenga sentido; Si tuviera múltiples hilos de conteo de giros, no necesariamente querría detenerlos a todos con la misma bandera. Sinfinished
embargo, desea escribir la inicialización de una manera que se compile solo para la inicialización, no en una tienda atómica. (Como lo está haciendo con lafinished = false;
sintaxis predeterminada del inicializador C ++ 17. Godbolt.org/z/EjoKgq ).