Estoy usando Cygwin GCC y ejecuto este código:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
unsigned u = 0;
void foo()
{
u++;
}
int main()
{
vector<thread> threads;
for(int i = 0; i < 1000; i++) {
threads.push_back (thread (foo));
}
for (auto& t : threads) t.join();
cout << u << endl;
return 0;
}
Compilado con la línea: g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o
.
Imprime 1000, que es correcto. Sin embargo, esperaba un número menor debido a que los subprocesos sobrescribían un valor incrementado previamente. ¿Por qué este código no sufre de acceso mutuo?
Mi máquina de prueba tiene 4 núcleos y no pongo restricciones al programa que conozco.
El problema persiste al reemplazar el contenido del compartido foo
por algo más complejo, p. Ej.
if (u % 3 == 0) {
u += 4;
} else {
u -= 1;
}
c++
race-condition
mafu
fuente
fuente
u
volver a escribir en la memoria. La CPU realmente hará cosas asombrosas como notar que la línea de memoria parau
no está en el caché de la CPU y reiniciará la operación de incremento. ¡Es por eso que pasar de x86 a otras arquitecturas puede ser una experiencia reveladora!while true; do res=$(./a.out); if [[ $res != 1000 ]]; then echo $res; break; fi; done;
imprime 999 o 998 en mi sistema.Respuestas:
foo()
es tan corto que cada hilo probablemente termina antes de que se genere el siguiente. Si agrega un sueño por un tiempo aleatoriofoo()
antes delu++
, puede comenzar a ver lo que espera.fuente
Es importante comprender que una condición de carrera no garantiza que el código se ejecute incorrectamente, simplemente que podría hacer cualquier cosa, ya que es un comportamiento indefinido. Incluyendo correr como se esperaba.
Particularmente en las máquinas X86 y AMD64, las condiciones de carrera en algunos casos rara vez causan problemas, ya que muchas de las instrucciones son atómicas y las garantías de coherencia son muy altas. Estas garantías se reducen algo en los sistemas multiprocesador donde se necesita el prefijo de bloqueo para que muchas instrucciones sean atómicas.
Si en su máquina, el incremento es una operación atómica, es probable que se ejecute correctamente aunque de acuerdo con el estándar del lenguaje sea Comportamiento indefinido.
Específicamente, espero que en este caso el código se esté compilando en un Fetch and Add atómico instrucción (ADD o XADD en el ensamblaje X86) que de hecho es atómico en sistemas de un solo procesador, sin embargo, en sistemas multiprocesador no se garantiza que sea atómico y un bloqueo sería necesario para hacerlo así. Si está ejecutando en un sistema multiprocesador, habrá una ventana donde los subprocesos podrían interferir y producir resultados incorrectos.
Específicamente, compilé su código para ensamblar usando https://godbolt.org/ y lo
foo()
compila para:Esto significa que solo está realizando una instrucción de adición que para un solo procesador será atómica (aunque como se mencionó anteriormente no es así para un sistema multiprocesador).
fuente
inc [u]
no es atómico. ElLOCK
prefijo es necesario para que una instrucción sea verdaderamente atómica. El OP simplemente está teniendo suerte. Recuerde que aunque le está diciendo a la CPU "agregue 1 a la palabra en esta dirección", la CPU todavía tiene que buscar, incrementar, almacenar ese valor y otra CPU puede hacer lo mismo simultáneamente, causando que el resultado sea incorrecto.Creo que no es tanto la cosa si pones un sueño antes o después del
u++
. Se trata más bien de que la operación seu++
traduce en un código que, en comparación con la sobrecarga de los subprocesos de generación que llamanfoo
, se realiza muy rápidamente, de modo que es poco probable que sea interceptado. Sin embargo, si "prolonga" la operaciónu++
, la condición de carrera será mucho más probable:resultado:
694
Por cierto: también lo intenté
y me dio la mayoría de las veces
1997
, pero a veces1995
.fuente
else u -= 1
ser ejecutado? Incluso en un entorno paralelo, el valor nunca debería no encajar%2
, ¿no es así?else u -= 1
se ejecuta una vez, la primera vez que se llama a foo (), cuando u == 0. Las 999 veces restantes u es impar yu += 2
se ejecuta dando como resultado u = -1 + 999 * 2 = 1997; es decir, la salida correcta. Una condición de carrera a veces hace que uno de los + = 2 sea sobrescrito por un hilo paralelo y obtienes 1995.Sufre de una condición de carrera. Ponga
usleep(1000);
antesu++;
enfoo
y veo diferente de salida (<1,000) cada vez.fuente
La respuesta probable que la razón por la condición de carrera no se manifestó para usted, a pesar de que hace existir, es que
foo()
es tan rápido, en comparación con el tiempo que se necesita para iniciar un hilo, que cada hilo acabados antes de la próxima puede incluso comenzar. Pero...Incluso con su versión original, el resultado varía según el sistema: lo probé a su manera en una Macbook (de cuatro núcleos) y, en diez ejecuciones, obtuve 1000 tres veces, 999 seis veces y 998 una vez. Entonces la carrera es algo rara, pero claramente presente.
Compilaste con
'-g'
, que tiene una forma de hacer desaparecer los errores. Volví a compilar su código, todavía sin cambios pero sin el'-g'
, y la carrera se volvió mucho más pronunciada: obtuve 1000 una vez, 999 tres veces, 998 dos veces, 997 dos veces, 996 una vez y 992 una vez.Re. la sugerencia de agregar un sueño - eso ayuda, pero (a) un tiempo de sueño fijo deja los hilos todavía sesgados por la hora de inicio (sujeto a la resolución del temporizador), y (b) un sueño aleatorio los distribuye cuando lo que queremos es acercarlos más juntos. En su lugar, los codificaría para esperar una señal de inicio, de modo que pueda crearlos todos antes de dejarlos trabajar. Con esta versión (con o sin
'-g'
), obtengo resultados en todas partes, tan bajos como 974 y no más altos que 998:fuente
-g
bandera no "hace desaparecer los errores" de ninguna manera. La-g
bandera en los compiladores GNU y Clang simplemente agrega símbolos de depuración al binario compilado. Esto le permite ejecutar herramientas de diagnóstico como GDB y Memcheck en sus programas con algunos resultados legibles por humanos. Por ejemplo, cuando Memcheck se ejecuta sobre un programa con una pérdida de memoria, no le dirá el número de línea a menos que el programa se haya creado con la-g
bandera.-O2
lugar de-g
". Pero dicho esto, si nunca ha tenido el placer de buscar un error que se manifestaría solo cuando se compila sin él-g
, considérese afortunado. Se puede suceder, con algunos de los muy desagradable de errores de aliasing sutiles. Lo he visto, aunque no recientemente, y podría creer que tal vez era una peculiaridad de un compilador propietario antiguo, así que te creeré, provisionalmente, acerca de las versiones modernas de GNU y Clang.-g
no le impide utilizar optimizaciones. por ejemplo,gcc -O3 -g
hace el mismo asm quegcc -O3
, pero con metadatos de depuración. gdb dirá "optimizado" si intenta imprimir algunas variables.-g
tal vez podría cambiar las ubicaciones relativas de algunas cosas en la memoria, si alguna de las cosas que agrega es parte de la.text
sección. Definitivamente ocupa espacio en el archivo de objeto, pero creo que después de vincularlo todo termina en un extremo del segmento de texto (no en la sección), o no forma parte de un segmento en absoluto. Quizás podría afectar dónde se asignan las cosas para las bibliotecas dinámicas.