thread multithread multi example español multithreading assembly atomic race-condition

multithreading - multithread - ¿Una instrucción de ensamblador siempre se ejecuta atómicamente?



thread java example (11)

  1. Las operaciones de incremento / decremento en variables enteras de 32 bits o menos en un solo procesador de 32 bits sin tecnología Hyper-Threading son atómicas.
  2. En un procesador con tecnología Hyper-Threading o en un sistema multiprocesador, NO se garantiza que las operaciones de incremento / decremento se ejecuten atómica.

Hoy me encontré con esta pregunta:

tienes un código

static int counter = 0; void worker() { for (int i = 1; i <= 10; i++) counter++; }

Si se llama al worker desde dos hilos diferentes, ¿qué valor tendrá el counter después de que ambos hayan finalizado?

Sé que en realidad podría ser cualquier cosa. Pero mis agallas internas me dicen que es muy probable que el counter++ se traduzca en instrucciones de ensamblador único, y si ambos hilos se ejecutan en el mismo núcleo, el counter será 20.

Pero, ¿y si esos hilos se ejecutan en diferentes núcleos o procesadores, podría haber una condición de carrera en su microcódigo? ¿Una instrucción de ensamblador siempre se puede ver como una operación atómica?


¡La respuesta es, depende!

Aquí hay algo de confusión, qué es una instrucción ensamblador. Normalmente, una instrucción de ensamblador se traduce en exactamente una instrucción de máquina. La exoneración se produce cuando utiliza macros, pero debe tenerlo en cuenta.

Dicho esto, la pregunta se reduce a una instrucción de máquina atómica?

En los viejos tiempos, lo era. Pero hoy, con CPUs complejas, instrucciones de larga duración, hyperthreading, ... no lo es. Algunas CPU garantizan que algunas instrucciones de incremento / disminución sean atómicas. La razón es que están limpios para una sincronización simple.

Además, algunos comandos de CPU no son tan problemáticos. Cuando tiene una búsqueda simple (de una pieza de datos que el procesador puede obtener de una sola pieza), la búsqueda en sí misma es, por supuesto, atómica, porque no hay nada que dividir en absoluto. Pero cuando tienes datos no alineados, se vuelven complicados nuevamente.

La respuesta es, depende. Lea atentamente el manual de instrucciones de la máquina del vendedor. En duda, no lo es!

Edit: Oh, lo vi ahora, también pides ++ counter. La frase "con la mayor probabilidad de ser traducida" no se puede confiar en absoluto. ¡Esto también depende en gran parte del compilador, por supuesto! Se vuelve más difícil cuando el compilador realiza diferentes optimizaciones.


Creo que obtendrás una condición de carrera en el acceso.

Si quieres asegurarte una operación atómica al incrementar el contador, necesitarás usar el contador ++.


En la mayoría de los casos, no . De hecho, en x86, puede realizar la instrucción

push [address]

que, en C, sería algo así como:

*stack-- = *address;

Esto realiza dos transferencias de memoria en una instrucción .

Eso es básicamente imposible de hacer en 1 ciclo de reloj, ¡no menos porque una transferencia de memoria tampoco es posible en un ciclo!


En muchos otros procesadores, la separación entre el sistema de memoria y el procesador es mayor. (a menudo estos procesadores pueden ser pequeños o grandes dependiendo del sistema de memoria, como ARM y PowerPC), esto también tiene consecuencias para el comportamiento atómico si el sistema de memoria puede reordenar las lecturas y escrituras.

Para este propósito, existen barreras de memoria ( http://en.wikipedia.org/wiki/Memory_barrier )

En resumen, aunque las instrucciones atómicas son suficientes en Intel (con los prefijos de bloqueo relevantes), se debe hacer más en no intel, ya que la E / S de memoria podría no estar en el mismo orden.

Este es un problema conocido cuando se transfieren soluciones "sin bloqueo" de Intel a otras arquitecturas.

(Tenga en cuenta que los sistemas multiprocesador (no multinúcleo) en x86 también parecen necesitar barreras de memoria, al menos en el modo de 64 bits.


Específicamente para x86, y con respecto a su ejemplo: counter++ , hay varias maneras de compilarlo. El ejemplo más trivial es:

inc counter

Esto se traduce en las siguientes microoperaciones:

  • cargar counter a un registro oculto en la CPU
  • incrementar el registro
  • almacenar el registro actualizado en el counter

Esto es esencialmente lo mismo que:

mov eax, counter inc eax mov counter, eax

Tenga en cuenta que si otro agente actualiza el counter entre la carga y la tienda, no se reflejará en el counter después de la tienda. Este agente podría ser otro hilo en el mismo núcleo, otro núcleo en la misma CPU, otra CPU en el mismo sistema o incluso algún agente externo que utiliza DMA (acceso directo a la memoria).

Si desea garantizar que este inc sea ​​atómico, use el prefijo de lock :

lock inc counter

lock garantiza que nadie puede actualizar el counter entre la carga y la tienda.

En cuanto a las instrucciones más complicadas, generalmente no se puede suponer que se ejecutarán atómicamente, a menos que sean compatibles con el prefijo de lock .


Invalidado por el comentario de Nathan: Si recuerdo correctamente mi ensamblador Intel x86, la instrucción INC solo funciona para registros y no funciona directamente para ubicaciones de memoria.

Entonces, un contador ++ no sería una sola instrucción en ensamblador (simplemente ignorando la parte de incremento posterior). Sería al menos tres instrucciones: cargar la variable del contador para registrar, incrementar el registro, cargar el registro de nuevo en el contador. Y eso es solo para la arquitectura x86.

En resumen, no confíe en que sea atómica a menos que esté especificada por la especificación del lenguaje y que el compilador que está utilizando admita las especificaciones.


No, no puedes asumir esto. A menos que se indique claramente en la especificación del compilador. Y, además, nadie puede garantizar que una única instrucción de ensamblador sea atómica. En la práctica, cada instrucción de ensamblador se traduce al número de operaciones de microcódigo - uops.
Además, el tema de la condición de carrera está estrechamente relacionado con el modelo de memoria (coherencia, secuencia, coherencia de lanzamiento, etc.), para cada uno la respuesta y el resultado podrían ser diferentes.


Otro problema es que si no declara la variable como volátil, el código generado probablemente no actualice la memoria en cada iteración de bucle, solo al final del bucle la memoria se actualizaría.


Puede que no sea una respuesta real a su pregunta, pero (suponiendo que este sea C # u otro lenguaje .NET) si quiere que el counter++ sea ​​realmente multi-hilo atómico, podría usar System.Threading.Interlocked.Increment(counter) .

Consulte otras respuestas para obtener información real sobre las diferentes formas en que / por qué el counter++ no puede ser atómico. ;-)


No siempre : en algunas arquitecturas, una instrucción de ensamblaje se traduce en una instrucción de código de máquina, mientras que en otras no.

Además , nunca puede suponer que el lenguaje de programa que está utilizando compila una línea de código aparentemente simple en una instrucción de ensamblaje. Además, en algunas arquitecturas, no se puede suponer que un código de máquina se ejecutará atómicamente.

Utilice las técnicas de sincronización adecuadas en su lugar, dependiendo del idioma en el que está codificando.