Escuché que i ++ no es una declaración segura para subprocesos, ya que en el ensamblaje se reduce a almacenar el valor original como una temperatura en algún lugar, incrementándolo y luego reemplazándolo, lo que podría ser interrumpido por un cambio de contexto.
Sin embargo, me pregunto acerca de ++ i. Por lo que puedo decir, esto se reduciría a una sola instrucción de ensamblaje, como 'agregar r1, r1, 1' y dado que es solo una instrucción, sería ininterrumpida por un cambio de contexto.
¿Alguien puede aclarar? Supongo que se está utilizando una plataforma x86.
c++
c
multithreading
Samoz
fuente
fuente
Respuestas:
Has escuchado mal. Bien puede ser que
"i++"
sea seguro para subprocesos para un compilador específico y una arquitectura de procesador específica, pero no es obligatorio en los estándares en absoluto. De hecho, dado que el subproceso múltiple no forma parte de los estándares ISO C o C ++ (a) , no puede considerar que nada sea seguro para subprocesos en función de lo que cree que se compilará.Es bastante factible que se
++i
pueda compilar en una secuencia arbitraria como:que no sería seguro para subprocesos en mi CPU (imaginaria) que no tiene instrucciones de incremento de memoria. O puede ser inteligente y compilarlo en:
donde
lock
deshabilita yunlock
habilita las interrupciones. Pero, incluso entonces, esto puede no ser seguro para subprocesos en una arquitectura que tiene más de una de estas CPU compartiendo memoria (lalock
posible que solo deshabilite las interrupciones para una CPU).El lenguaje en sí (o las bibliotecas para él, si no está integrado en el lenguaje) proporcionará construcciones seguras para subprocesos y debe usarlas en lugar de depender de su comprensión (o posiblemente malentendido) de qué código de máquina se generará.
Cosas como Java
synchronized
ypthread_mutex_lock()
(disponible para C / C ++ en algunos sistemas operativos) son lo que necesita examinar (a) .(a) Esta pregunta se hizo antes de que se completaran los estándares C11 y C ++ 11. Esas iteraciones ahora han introducido el soporte de subprocesos en las especificaciones del lenguaje, incluidos los tipos de datos atómicos (aunque ellos, y los subprocesos en general, son opcionales, al menos en C).
fuente
No se puede hacer una declaración general sobre ++ i o i ++. ¿Por qué? Considere incrementar un número entero de 64 bits en un sistema de 32 bits. A menos que la máquina subyacente tenga una instrucción de cuatro palabras "cargar, incrementar, almacenar", incrementar ese valor requerirá múltiples instrucciones, cualquiera de las cuales puede ser interrumpida por un cambio de contexto de hilo.
Además,
++i
no siempre "agrega uno al valor". En un lenguaje como C, incrementar un puntero en realidad agrega el tamaño de la cosa apuntada. Es decir, sii
es un puntero a una estructura de++i
32 bytes , agrega 32 bytes. Mientras que casi todas las plataformas tienen una instrucción de "valor de incremento en la dirección de memoria" que es atómica, no todas tienen una instrucción atómica de "agregar valor arbitrario al valor en la dirección de memoria".fuente
Ambos son subprocesos inseguros.
Una CPU no puede hacer matemáticas directamente con la memoria. Lo hace indirectamente cargando el valor de la memoria y haciendo los cálculos con los registros de la CPU.
yo ++
++ i
Para ambos casos, existe una condición de carrera que da como resultado el valor i impredecible.
Por ejemplo, supongamos que hay dos subprocesos ++ i concurrentes y cada uno usa los registros a1, b1 respectivamente. Y, con el cambio de contexto ejecutado como el siguiente:
Como resultado, i no se convierte en i + 2, se convierte en i + 1, lo cual es incorrecto.
Para remediar esto, las CPU modernas proporcionan algún tipo de BLOQUEO, DESBLOQUEO de instrucciones de la CPU durante el intervalo en que se deshabilita el cambio de contexto.
En Win32, use InterlockedIncrement () para hacer i ++ para la seguridad de subprocesos. Es mucho más rápido que confiar en mutex.
fuente
Si comparte incluso un int entre subprocesos en un entorno de varios núcleos, necesita las barreras de memoria adecuadas en su lugar. Esto puede significar usar instrucciones entrelazadas (ver InterlockedIncrement en win32 por ejemplo), o usar un lenguaje (o compilador) que ofrezca ciertas garantías de seguridad para subprocesos. Con el reordenamiento de instrucciones a nivel de CPU, las cachés y otros problemas, a menos que tenga esas garantías, no asuma que cualquier cosa compartida entre subprocesos es segura.
Editar: Una cosa que puede asumir con la mayoría de las arquitecturas es que si está tratando con palabras individuales alineadas correctamente, no terminará con una sola palabra que contenga una combinación de dos valores que se combinaron. Si dos escrituras se superponen, una ganará y la otra se descartará. Si tiene cuidado, puede aprovechar esto y ver que ++ i o i ++ son seguros para subprocesos en la situación de un solo escritor / lector múltiple.
fuente
Si desea un incremento atómico en C ++, puede usar bibliotecas C ++ 0x (el
std::atomic
tipo de datos) o algo como TBB.Hubo un tiempo en que las pautas de codificación GNU decían que actualizar tipos de datos que caben en una palabra era "generalmente seguro", pero ese consejo es incorrecto para máquinas SMP,
incorrecto para algunas arquitecturasy incorrecto cuando se usa un compilador optimizador.Para aclarar el comentario de "actualización del tipo de datos de una palabra":
Es posible que dos CPU en una máquina SMP escriban en la misma ubicación de memoria en el mismo ciclo y luego intenten propagar el cambio a las otras CPU y al caché. Incluso si solo se escribe una palabra de datos, por lo que las escrituras solo toman un ciclo para completarse, también ocurren simultáneamente, por lo que no puede garantizar qué escritura se realiza correctamente. No obtendrá datos parcialmente actualizados, pero una escritura desaparecerá porque no hay otra forma de manejar este caso.
Compare e intercambie correctamente las coordenadas entre varias CPU, pero no hay razón para creer que cada asignación de variable de tipos de datos de una palabra utilizará comparar e intercambiar.
Y aunque un compilador de optimización no afecta cómo se compila una carga / almacenamiento, puede cambiar cuando ocurre la carga / almacenamiento, causando serios problemas si espera que sus lecturas y escrituras ocurran en el mismo orden en que aparecen en el código fuente ( el bloqueo más famoso con doble verificación no funciona en vainilla C ++).
NOTA Mi respuesta original también decía que la arquitectura Intel de 64 bits estaba rota al tratar con datos de 64 bits. Eso no es cierto, así que edité la respuesta, pero mi edición afirmaba que los chips PowerPC estaban rotos. Eso es cierto cuando se leen valores inmediatos (es decir, constantes) en registros (consulte las dos secciones denominadas "Cargando punteros" en la lista 2 y la lista 4). Pero hay una instrucción para cargar datos de la memoria en un ciclo (
lmw
), así que eliminé esa parte de mi respuesta.fuente
En x86 / Windows en C / C ++, no debe asumir que es seguro para subprocesos. Debe usar InterlockedIncrement () e InterlockedDecrement () si necesita operaciones atómicas.
fuente
Si el lenguaje de programación no dice nada sobre las discusiones, sin embargo, se ejecuta en una plataforma multiproceso, ¿cómo puede cualquier construcción de lenguaje ser segura para subprocesos?
Como otros señalaron: debe proteger cualquier acceso multiproceso a variables mediante llamadas específicas de la plataforma.
Existen bibliotecas que abstraen la especificidad de la plataforma, y el próximo estándar C ++ ha adaptado su modelo de memoria para hacer frente a los subprocesos (y por lo tanto puede garantizar la seguridad de los subprocesos).
fuente
Incluso si se reduce a una sola instrucción de ensamblaje, incrementando el valor directamente en la memoria, todavía no es seguro para subprocesos.
Al incrementar un valor en la memoria, el hardware realiza una operación de "lectura-modificación-escritura": lee el valor de la memoria, lo incrementa y lo vuelve a escribir en la memoria. El hardware x86 no tiene forma de incrementarse directamente en la memoria; la RAM (y las cachés) solo puede leer y almacenar valores, no modificarlos.
Ahora suponga que tiene dos núcleos separados, ya sea en sockets separados o compartiendo un solo socket (con o sin un caché compartido). El primer procesador lee el valor y, antes de que pueda volver a escribir el valor actualizado, el segundo procesador lo lee. Después de que ambos procesadores vuelvan a escribir el valor, se habrá incrementado solo una vez, no dos.
Hay una forma de evitar este problema; Los procesadores x86 (y la mayoría de los procesadores multinúcleo que encontrará) son capaces de detectar este tipo de conflicto en el hardware y secuenciarlo, de modo que toda la secuencia de lectura-modificación-escritura parezca atómica. Sin embargo, dado que esto es muy costoso, solo se hace cuando lo solicita el código, en x86 generalmente a través del
LOCK
prefijo. Otras arquitecturas pueden hacer esto de otras formas, con resultados similares; por ejemplo, comparación e intercambio atómicos / condicional de almacenamiento / ligado a carga (los procesadores x86 recientes también tienen este último).Tenga en cuenta que el uso
volatile
no ayuda aquí; solo le dice al compilador que la variable podría haber sido modificada externamente y las lecturas de esa variable no deben almacenarse en caché en un registro ni optimizarse. No hace que el compilador utilice primitivas atómicas.La mejor manera es usar primitivas atómicas (si su compilador o bibliotecas las tienen), o hacer el incremento directamente en ensamblador (usando las instrucciones atómicas correctas).
fuente
Nunca asuma que un incremento se compilará en una operación atómica. Utilice InterlockedIncrement o cualquier función similar que exista en su plataforma de destino.
Editar: Acabo de buscar esta pregunta específica y el incremento en X86 es atómico en sistemas de un solo procesador, pero no en sistemas multiprocesador. Usar el prefijo de bloqueo puede hacerlo atómico, pero es mucho más portátil solo para usar InterlockedIncrement.
fuente
De acuerdo con esta lección de ensamblaje sobre x86, puede agregar atómicamente un registro a una ubicación de memoria , por lo que potencialmente su código puede ejecutar atómicamente '++ i' o 'i ++'. Pero como se dijo en otra publicación, C ansi no aplica atomicidad a la operación '++', por lo que no puede estar seguro de lo que generará su compilador.
fuente
El estándar C ++ de 1998 no tiene nada que decir acerca de los subprocesos, aunque el siguiente estándar (que vence este año o el próximo) sí. Por lo tanto, no se puede decir nada inteligente sobre la seguridad de las operaciones de subprocesos sin hacer referencia a la implementación. No solo se utiliza el procesador, sino la combinación del compilador, el sistema operativo y el modelo de subproceso.
En ausencia de documentación que indique lo contrario, no asumiría que ninguna acción es segura para subprocesos, particularmente con procesadores de múltiples núcleos (o sistemas de múltiples procesadores). Tampoco confiaría en las pruebas, ya que es probable que los problemas de sincronización de subprocesos surjan solo por accidente.
Nada es seguro para subprocesos a menos que tenga documentación que indique que es para el sistema particular que está utilizando.
fuente
Lanzar i en el almacenamiento local de subprocesos; no es atómico, pero luego no importa.
fuente
AFAIK, según el estándar C ++, las lecturas / escrituras en un
int
son atómicas.Sin embargo, todo lo que esto hace es deshacerse del comportamiento indefinido asociado con una carrera de datos.
Pero aún habrá una carrera de datos si ambos hilos intentan incrementar
i
.Imagina el siguiente escenario:
Deje
i = 0
inicialmente:El subproceso A lee el valor de la memoria y lo almacena en su propia caché. El hilo A incrementa el valor en 1.
El subproceso B lee el valor de la memoria y lo almacena en su propia caché. El hilo B incrementa el valor en 1.
Si todo esto es un solo hilo, obtendrás
i = 2
en la memoria.Pero con ambos subprocesos, cada subproceso escribe sus cambios y, por lo tanto, el subproceso A escribe de
i = 1
nuevo en la memoria y el subproceso B escribei = 1
en la memoria.Está bien definido, no hay destrucción o construcción parcial ni ningún tipo de desgarro de un objeto, pero sigue siendo una carrera de datos.
Para incrementar atómicamente
i
puedes usar:std::atomic<int>::fetch_add(1, std::memory_order_relaxed)
Se puede usar un orden relajado porque no nos importa dónde se lleva a cabo esta operación, lo único que nos importa es que la operación de incremento sea atómica.
fuente
Dice "es sólo una instrucción, sería ininterrumpida mediante un cambio de contexto". - eso está muy bien para una sola CPU, pero ¿qué pasa con una CPU de doble núcleo? Entonces realmente puede tener dos subprocesos accediendo a la misma variable al mismo tiempo sin ningún cambio de contexto.
Sin conocer el idioma, la respuesta es ponerlo a prueba.
fuente
Creo que si la expresión "i ++" es la única en una declaración, es equivalente a "++ i", el compilador es lo suficientemente inteligente como para no mantener un valor temporal, etc. Entonces, si puede usarlos indistintamente (de lo contrario, ganó no preguntes cuál usar), no importa el que uses, ya que son casi iguales (excepto por la estética).
De todos modos, incluso si el operador de incremento es atómico, eso no garantiza que el resto del cálculo sea consistente si no usa los bloqueos correctos.
Si desea experimentar por sí mismo, escriba un programa en el que N subprocesos incrementen simultáneamente una variable compartida M veces cada uno ... si el valor es menor que N * M, entonces se sobrescribió algún incremento. Pruébalo tanto con preincremento como con postincremento y cuéntanos ;-)
fuente
Para un contador, recomiendo usar el idioma de comparar e intercambiar, que es tanto sin bloqueo como seguro para subprocesos.
Aquí está en Java:
fuente