c++ - suma - ¿Es necesario usar std:: atomic para indicar que un hilo ha finalizado su ejecución?
suma con hilos en c (4)
Me gustaría comprobar si un std::thread
ha finalizado su ejecución. Buscando stackoverflow encontré la siguiente question que aborda este problema. La respuesta aceptada propone que el subproceso de trabajo establezca una variable justo antes de salir y que el subproceso principal verifique esta variable. Aquí hay un ejemplo de trabajo mínimo de tal solución:
#include <unistd.h>
#include <thread>
void work( bool* signal_finished ) {
sleep( 5 );
*signal_finished = true;
}
int main()
{
bool thread_finished = false;
std::thread worker(work, &thread_finished);
while ( !thread_finished ) {
// do some own work until the thread has finished ...
}
worker.join();
}
Alguien que comentó la respuesta aceptada afirma que no se puede usar una simple variable bool
como señal, el código se rompió sin una barrera de memoria y el uso de std::atomic<bool>
sería correcto. Mi conjetura inicial es que esto está mal y un simple error es suficiente, pero quiero asegurarme de que no me esté perdiendo algo. ¿Necesita el código anterior un std::atomic<bool>
para ser correcto?
Supongamos que el subproceso principal y el trabajador se ejecutan en diferentes CPU en diferentes sockets. Lo que creo que sucedería es que el hilo principal lee thread_finished
de la memoria caché de la CPU. Cuando el trabajador lo actualiza, el protocolo de coherencia de la memoria caché se encarga de escribir los cambios de los trabajadores en la memoria global y de invalidar la memoria caché de la CPU del subproceso principal para que tenga que leer el valor actualizado de la memoria global. ¿No es el punto de coherencia del caché para hacer que el código como el anterior funcione?
Alguien que comentó la respuesta aceptada afirma que no se puede usar una simple variable bool como señal, el código se rompió sin una barrera de memoria y el uso de std :: atomic sería correcto.
El comentarista tiene razón: un bool
simple no es suficiente, porque las escrituras no atómicas del hilo que establece thread_finished
en true
se pueden reordenar.
Considere un hilo que establece una variable estática x
en un número muy importante, y luego señala su salida, como esto:
x = 42;
thread_finished = true;
Cuando el subproceso principal ve que thread_finished
establece en true
, se supone que el subproceso de trabajo ha finalizado. Sin embargo, cuando su hilo principal examina x
, puede encontrarlo configurado en un número incorrecto, porque las dos escrituras anteriores han sido reordenadas.
Por supuesto, esto es solo un ejemplo simplificado para ilustrar el problema general. El uso de std::atomic
para su variable thread_finished
agrega una barrera de memoria , asegurándose de que todas las escrituras antes de que se realicen. Esto soluciona el problema potencial de escrituras fuera de orden.
Otro problema es que las lecturas de las variables no volátiles se pueden optimizar, por lo que el hilo principal nunca notaría el cambio en el indicador thread_finished
.
thread_finished
volátil no solucione el problema; de hecho, volatile no se debe usar junto con el enhebrado, ya que está diseñado para trabajar con hardware mapeado en memoria. Los algoritmos de coherencia de caché no están presentes en todas partes, ni son perfectos. El problema que rodea a thread_finished
es que un hilo intenta escribirle un valor mientras que otro hilo intenta leerlo. Esta es una carrera de datos, y si los accesos no están secuenciados, se traduce en un comportamiento indefinido.
No está bien. Optimizador puede optimizar
while ( !thread_finished ) {
// do some own work until the thread has finished ...
}
a:
if(!thread_finished)
while (1) {
// do some own work until the thread has finished ...
}
Suponiendo que se pueda probar, que "algunos trabajos propios" no cambian thread_finished
.
Usar un bool
crudo no es suficiente.
La ejecución de un programa contiene una carrera de datos si contiene dos acciones en conflicto en subprocesos diferentes, al menos una de las cuales no es atómica, y ninguna sucede antes de la otra. Cualquier carrera de datos de este tipo resulta en un comportamiento indefinido. § 1.10 p21
Dos evaluaciones de expresiones entran en conflicto si una de ellas modifica una ubicación de memoria (1.7) y la otra accede o modifica la misma ubicación de memoria. § 1.10 p4
Su programa contiene una carrera de datos en la que el subproceso de trabajo se escribe en el bool y el subproceso principal se lee en él, pero no hay una relación formal entre las operaciones.
Hay varias formas diferentes de evitar la carrera de datos, incluido el uso de std::atomic<bool>
con los ordenamientos de memoria adecuados, el uso de una barrera de memoria, o la sustitución del bool con una variable de condición.