tiene proyecto programas para informacion dev depurar depuración depuracion c++ multithreading thread-safety data-race

programas - el proyecto no tiene informacion para depuracion dev c++



El programa de subprocesos múltiples se atascó en modo optimizado pero se ejecuta normalmente en-O0 (3)

Dos subprocesos, que acceden a una variable no atómica, no protegida, son U.B. Esto concierne a finished . Puede hacer que finished tipo std::atomic<bool> para arreglar esto.

Mi solución:

#include <iostream> #include <future> #include <atomic> static std::atomic<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; }

Salida:

result =1023045342 main thread id=140147660588864

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 rasgado no es lo único que le ofrece std::atomic . 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 sin vigilancia, no atómico puede causar problemas adicionales:

  • El compilador podría decidir optimizar la variable en un registro o incluso CSE múltiples accesos en uno y levantar una carga de un bucle.
  • La variable puede almacenarse en caché para un núcleo de CPU. (En la vida real, las CPU tienen cachés coherentes . Esto no es un problema real, pero el estándar C ++ es lo suficientemente flojo como para cubrir implementaciones hipotéticas de C ++ en memoria compartida no coherente donde atomic<bool> con almacenamiento / carga de memory_order_relaxed funcionaría, pero donde volatile no. Usar 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 discusión en evolución sobre la posible relación de los volatile con este tema. Por lo tanto, me gustaría gastar mis dos centavos:

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 segundo. Pero se atascó y no imprime nada en modo Release o -O1 -O2 -O3 .


En aras de la integridad en la curva de aprendizaje; Debe evitar el uso de variables globales. Sin embargo, hizo un buen trabajo al hacerlo estático, por lo que será local para la unidad de traducción.

Aquí hay un ejemplo:

class ST { public: int func() { size_t i = 0; while (!finished) ++i; return i; } void setFinished(bool val) { finished = val; } private: std::atomic<bool> finished = false; }; int main() { ST st; auto result=std::async(std::launch::async, &ST::func, std::ref(st)); std::this_thread::sleep_for(std::chrono::seconds(1)); st.setFinished(true); std::cout<<"result ="<<result.get(); std::cout<<"/nmain thread id="<<std::this_thread::get_id()<<std::endl; }

Live on wandbox


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.

godbolt.org/z/Tl44iN su código en godbolt.org/z/Tl44iN usando el nivel de optimización 1 ( -O1 ). Su función se compila así:

func(): cmp BYTE PTR finished[rip], 0 jne .L4 .L5: jmp .L5 .L4: mov eax, 0 ret

¿Entonces, Que esta pasando aquí? Primero, tenemos una comparación: cmp BYTE PTR finished[rip], 0 - esto verifica si finished es falso o no.

Si no es falso (también conocido como verdadero), debemos salir del bucle en la primera ejecución. Esto se logra mediante jne .L4 que salta cuando no se puede etiquetar .L4 donde el valor de i ( 0 ) se almacena en un registro para su uso posterior y la función regresa.

Sin embargo, si es falso, pasamos a

.L5: jmp .L5

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 variable finished puede cambiar potencialmente durante la ejecución de la función, ve que finished no es modificada por la función misma 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 ni la comparación:

func(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], 0 .L148: movzx eax, BYTE PTR finished[rip] test al, al jne .L147 add QWORD PTR [rbp-8], 1 jmp .L148 .L147: mov rax, QWORD PTR [rbp-8] pop rbp ret

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 que uno se desvía de 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.