2011-01-03 6 views
7

¿Hay algún problema con varios subprocesos que utilizan la misma ubicación de memoria entera entre pthreads en un programa C sin utilidades de sincronización?¿Puede un entero ser compartido entre hilos de forma segura?

Para simplificar el problema,

  • sólo un hilo escribirá al entero
  • múltiples hilos leerán el entero

Este pseudo-C ilustra lo que estoy pensando

void thread_main(int *a) { 
    //wait for something to finish 
    //dereference 'a', make decision based on its value 
} 

int value = 0; 

for (int i=0; i<10; i++) 
    pthread_create(NULL,NULL,thread_main,&value); 
} 
// do something 
value = 1; 

Supongo que es seguro, ya que un entero ocupa una palabra del procesador, una d leer/escribir en una palabra debe ser la más atómica de las operaciones, ¿verdad?

+2

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. –

Respuesta

12

Su pseudocódigo NO es seguro.

Aunque el acceso a un entero de tamaño de palabra es realmente atómico, lo que significa que nunca verá un valor intermedio, pero "antes de escribir" o "después de escribir", esto no es suficiente para su algoritmo.

Confía en el orden relativo de la escritura en a y realiza algún otro cambio que despierta el hilo. Esta no es una operación atómica y no está garantizada en los procesadores modernos.

Necesita algún tipo de cerca de memoria para evitar el reordenamiento de escritura. De lo contrario, no se garantiza que otros hilos vean el nuevo valor.

+2

En x86 y cualquier plataforma con requisitos de ordenación de memoria, o en una máquina no SMP donde todos los hilos se ejecutan en una sola CPU, hacer que la variable sea "volátil" es suficiente. Otro enfoque sería escribir la variable a través de una función externa que toma un puntero a la variable y su nuevo valor. Esto evitará el reordenamiento, y en las arquitecturas patológicas, podría agregar una operación de valla/barrera de memoria a la implementación específica de la CPU de esta función. –

+0

-1 porque esta respuesta es incorrecta. – johnnycrash

+0

Lo aclararé. Esto funcionaría bien en x86 usando gcc para compilar. Si el optimizador usó optimizaciones de registro en 'valor' ... use volátil, aunque gcc evita la opción de registro con código para acceder a variables globales. Incluso si coloca mutexes y bloqueos alrededor de 'valor', el compilador aún puede registrar la optimización. Además, la condición de carrera para adquirir el candado es la misma que la condición de carrera para leer el valor.¡Finalmente, está sugiriendo un código adicional para forzar la atomicidad de una operación que ya es atómica! – johnnycrash

0

No contaría con eso. El compilador puede emitir código que asume que sabe cuál es el valor de 'valor' en un momento dado en un registro de la CPU sin volver a cargarlo desde la memoria.

+0

¿Lo tiene? ¿Es necesario usar 'volátil' en cualquier variable compartida, o son los compiladores lo suficientemente inteligentes como para determinarlo? – Kos

+0

'volátil' afecta a los valores de caché en los registros, pero al menos en algunos compiladores no afecta el reordenamiento interno de escritura del procesador, que puede ser igualmente fatal. –

+0

@Kos: No es práctico para el compilador determinar si una variable se ha modificado externamente, porque el compilador no sabe nada acerca de los subprocesos; no puede saber si se ejecutarán dos funciones simultáneamente. Siempre podría asumir el peor de los casos y nunca optimizar una lectura de memoria, ¡pero eso en lugar de derrotar al objeto de optimización! Incluso si fuera consciente de los hilos, el compilador no vería una variable modificada en una unidad de compilación separada. La cantidad de análisis estático requerido para * demostrar * el acceso concurrente en cualquier sistema moderadamente complejo es * enorme *. – Clifford

-1

Hm, supongo que es seguro, pero ¿por qué no declaras una función que devuelve el valor a los otros hilos, ya que solo lo leerán?

Porque la simple idea de pasar punteros a hilos separados ya es un error de seguridad, en mi humilde opinión. Lo que te estoy diciendo es: ¿por qué dar una dirección entera (modificable, accesible al público) cuando solo necesitas el valor?

+1

Esa función tiene que obtener el valor de alguna parte. Mover la lectura de la función principal del hilo a alguna función auxiliar no cambia nada (a menos que el ayudante esté en una unidad de compilación diferente y la optimización de todo el programa esté deshabilitada, en cuyo caso la incapacidad de optimizar a través de la llamada a función podría tener efecto). –

+0

¿No es al menos evitar arrojar punteros? Y eso también ayuda si tiene que mover o cambiar la dirección de memoria de los datos. Si tiene punteros en n instancias, tendrá que enviarles la nueva dirección uno por uno. Por wat, entendí tu punto :) – Giuliano

-1

Suponga que lo primero que hace en thread func es dormir por un segundo. Así que el valor después de eso será definitivamente 1.

1

A diferencia de Java donde inicia explícitamente un hilo, los hilos posix comienzan a ejecutarse de inmediato.
Por lo tanto, no hay garantía de que el valor que establece en 1 en la función principal (suponiendo que es lo que refiere en su pseudocódigo) se ejecutará antes o después de que los hilos intenten acceder a él.
Si bien es seguro leer el entero al mismo tiempo, necesita hacer alguna sincronización si necesita escribir en el valor para ser utilizado por los hilos.
De lo contrario, no hay garantía de cuál será el valor que leerán (para actuar en función del valor que tenga en cuenta).
No se le debe hacer suposiciones sobre multithreading e.g.that hay algún tipo de procesamiento en cada hilo se acaben acceder al valor etc.
no hay garantías

-1

En cualquier instante al menos debe declarar la variable volatile compartido.Sin embargo, en todos los casos debería preferir alguna otra forma de thread IPC o sincronización; en este caso, parece que condition variable es lo que realmente necesita.

0

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.

+0

"El reordenamiento de escritura del procesador no va a afectar a un hilo leyendo un valor global establecido por otro hilo". En realidad, eso es exactamente lo que afecta. Bueno, un hilo que lee valores múltiples establecidos por otro (s) hilo (s). El punto es que cuando el hilo de trabajo ve que 'a' es verdadero, no puede confiar en el valor de ningún otro estado a menos que se use una barrera de memoria. –

+0

@Ben Gracias. Edité mi publicación para dejar eso en claro ahora. ¿Cómo se ve eso? – johnnycrash

+1

Creo que el lector necesita poner la cerca después de probar 'value' y antes de usar las variables que' value' se usa para sincronizar. Un par de cosas a tener en cuenta: las CPU x86 tienen garantías de pedido mucho más sólidas que las que ofrece el estándar C++, y muchas funciones del sistema operativo incluyen una barrera de memoria. Por lo tanto, es muy posible que su código actual nunca falle (pero debido a los detalles de implementación en lugar de cualquier garantía, y esos detalles pueden cambiar en futuras plataformas). –

Cuestiones relacionadas