EDIT: Ben es correcta (y yo soy un idiota por decir que él no lo era) que existe la posibilidad de que la CPU reordenar las instrucciones y ejecutarlas por múltiples tuberías al mismo tiempo, . Esto significa que el valor = 1 podría establecerse antes de que finalice la tubería que realiza "el trabajo". En mi defensa (¿no es un completo idiota?) Nunca he visto que esto suceda en la vida real y tenemos una extensa biblioteca de hilos y hacemos pruebas exhaustivas a largo plazo y este patrón se usa en todas partes. Lo habría visto si sucediera, pero ninguna de nuestras pruebas falla o produce una respuesta incorrecta. Pero ... Ben tiene razón, la posibilidad existe. Probablemente esté sucediendo todo el tiempo en nuestro código, pero el reordenamiento no establece indicadores lo suficientemente temprano como para que los consumidores de los datos protegidos por los indicadores puedan usar los datos antes de que finalicen. Voy a cambiar nuestro código para incluir barreras, porque no hay garantía de que esto continuará funcionando en la naturaleza. Creo que la solución correcta es similar a esto:
Hilos que lea el valor:
...
if (value)
{
__sync_synchronize(); // don't pipeline any of the work until after checking value
DoSomething();
}
...
El hilo que establece el valor:
...
DoStuff()
__sync_synchronize(); // Don't pipeline "setting value" until after finishing stuff
value = 1; // Stuff Done
...
Dicho esto, me encontré this estar una explicación simple de las barreras.
COMPILER BARRIER Las barreras de memoria afectan a la CPU. Las barreras del compilador afectan al compilador. Volátil no evitará que el compilador vuelva a ordenar el código. Here para más información.
Creo que se puede utilizar este código para mantener gcc de reordenar el código durante el tiempo de compilación:
#define COMPILER_BARRIER() __asm__ __volatile__ ("" ::: "memory")
así que quizás esto es lo que realmente se debe hacer?
#define GENERAL_BARRIER() do { COMPILER_BARRIER(); __sync_synchronize(); } while(0)
Hilos que leen el valor:
...
if (value)
{
GENERAL_BARRIER(); // don't pipeline any of the work until after checking value
DoSomething();
}
...
El hilo que establece el valor:
...
DoStuff()
GENERAL_BARRIER(); // Don't pipeline "setting value" until after finishing stuff
value = 1; // Stuff Done
...
Usando GENERAL_BARRIER() mantiene gcc de volver a ordenar el código y también mantiene el CPU de reordenar el código. Ahora, me pregunto si gcc no volverá a ordenar el código sobre su barrera de memoria integrada, __sync_synchronize(), lo que haría que el uso de COMPILER_BARRIER sea redundante.
X86 Como señala Ben, arquitecturas diferentes tienen diferentes reglas sobre cómo se reorganizan código en las tuberías de ejecución. Intel parece ser bastante conservador. Por lo tanto, es posible que las barreras no sean tan necesarias en Intel. No es una buena razón para evitar las barreras, ya que eso podría cambiar.
POSTE ORIGINAL: Hacemos esto todo el tiempo. es perfectamente seguro (no para todas las situaciones, pero mucho). Nuestra aplicación se ejecuta en miles de servidores en una gran granja con 16 instancias por servidor y no tenemos condiciones de carrera. Tiene razón al preguntarse por qué las personas usan mutexes para proteger operaciones ya atómicas.En muchas situaciones, el bloqueo es una pérdida de tiempo. Leer y escribir en enteros de 32 bits en la mayoría de las arquitecturas es atómico. ¡No intentes eso con campos de bits de 32 bits!
El reordenamiento de la escritura del procesador no afectará a un hilo que lea un valor global establecido por otro hilo. De hecho, el resultado que utiliza bloqueos es el mismo que el resultado, no sin bloqueos. Si ganas la carrera y compruebas el valor antes de que cambie ... bueno, eso es lo mismo que ganar la carrera para bloquear el valor para que nadie más pueda cambiarlo mientras lo lees. Funcionalmente lo mismo.
La palabra clave volátil le dice al compilador que no almacene un valor en un registro, sino que siga consultando la ubicación de la memoria original. esto no debería tener ningún efecto a menos que esté optimizando el código. Hemos descubierto que el compilador es bastante inteligente al respecto y no se ha encontrado con una situación en la que volátil haya cambiado algo. El compilador parece ser bastante bueno para encontrar candidatos para la optimización de registros. Sospecho que la palabra clave const podría fomentar la optimización del registro en una variable.
El compilador puede reordenar código en una función si sabe que el resultado final no será diferente. No he visto al compilador hacer esto con variables globales, porque el compilador no tiene idea de cómo el cambio del orden de una variable global afectará el código fuera de la función inmediata.
Si una función está actuando, puede controlar el nivel de optimización en el nivel de función usando __attrribute__.
Ahora, dicho eso, si usa esa bandera como una puerta de enlace para permitir que solo un hilo de un grupo realice algún trabajo, que no funcionará. Ejemplo: El hilo A y el hilo B pueden leer el indicador. El hilo A se programa. El hilo B establece el indicador en 1 y comienza a funcionar. El subproceso A se activa y establece el indicador en 1 y comienza a funcionar. Ooops! Para evitar bloqueos y aún así hacer algo así, debe analizar operaciones atómicas, específicamente gcc atomic builtins, como __sync_bool_compare_and_swap (value, old, new). Esto le permite establecer value = new si el valor es actualmente old. En el ejemplo anterior, si value = 1, solo un hilo (A o B) podría ejecutar __sync_bool_compare_and_swap (& value, 1, 2) y cambiar el valor de 1 a 2. El hilo perdedor fallaría. __sync_bool_compare_and_swap devuelve el éxito de la operación.
En el fondo, hay un "bloqueo" cuando se usan las construcciones internas atómicas, pero es una instrucción de hardware y muy rápida en comparación con el uso de mutexes.
Dicho esto, utilice mutexes cuando tenga que cambiar muchos valores al mismo tiempo. Las operaciones atómicas (a partir de hoy) solo funcionan cuando todos los datos que tienen que cambiar atómicamente pueden caber en un 8,16,32,64 contiguo o 128 bits.
La sincronización y la exclusión mutua son sutilmente diferentes. Su código garantiza la exclusión mutua debido a la carga/almacenamiento atómico de la palabra del procesador. Sin embargo, no ha resaltado ningún requisito de sincronización (lo que sucede antes/después de) en su pregunta. –