c++ - pthread - He oído que i++ no es seguro para subprocesos, ¿es++ i thread-safe?
thread c++ ejemplo (15)
Ambos son inseguros.
Una CPU no puede hacer matemática directamente con la memoria. Lo hace indirectamente cargando el valor de la memoria y haciendo los cálculos con los registros de la CPU.
i ++
register int a1, a2;
a1 = *(&i) ; // One cpu instruction: LOAD from memory location identified by i;
a2 = a1;
a1 += 1;
*(&i) = a1;
return a2; // 4 cpu instructions
++ i
register int a1;
a1 = *(&i) ;
a1 += 1;
*(&i) = a1;
return a1; // 3 cpu instructions
Para ambos casos, hay una condición de carrera que resulta en el valor i impredecible.
Por ejemplo, supongamos que hay dos hilos ++ i concurrentes con cada uno usando el registro a1, b1 respectivamente. Y, con el cambio de contexto ejecutado de la siguiente manera:
register int a1, b1;
a1 = *(&i);
a1 += 1;
b1 = *(&i);
b1 += 1;
*(&i) = a1;
*(&i) = b1;
En resultado, no me convierto en i + 2, se convierte en i + 1, que es incorrecto.
Para remediar esto, las CPU moden proporcionan algún tipo de instrucciones LOCK, UNLOCK cpu durante el intervalo en que se deshabilita el cambio de contexto.
En Win32, use InterlockedIncrement () para hacer i ++ para seguridad de subprocesos. Es mucho más rápido que confiar en mutex.
He oído que i ++ no es una sentencia 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 verse 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 ininterrumpible por un cambio de contexto.
¿Alguien puede aclarar? Supongo que se está utilizando una plataforma x86.
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. Por lo tanto, si puede usarlos de manera intercambiable (de lo contrario, ganó no pregunte cuál usar), no importa el que use, ya que son casi iguales (excepto 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 donde N hilos incrementen simultáneamente una variable compartida M veces cada ... si el valor es menor que N * M, entonces se sobrescribió algún incremento. Pruébalo con preincremento y postincremento y cuéntanos ;-)
De acuerdo con esta lección de ensamblaje en x86, puede agregar de forma atómica un registro a una ubicación de memoria , por lo que su código potencialmente puede ejecutar atómicamente ''++ i'' ou ''i ++''. Pero como se dijo en otra publicación, el C ansi no aplica la atomicidad a la operación ''++'', por lo que no puede estar seguro de lo que generará su compilador.
El estándar de 1998 C ++ no tiene nada que decir sobre los hilos, aunque el siguiente estándar (debido a este año o al siguiente) sí lo hace. Por lo tanto, no puede decir nada inteligente acerca de la seguridad de las operaciones sin hacer referencia a la implementación. No se trata solo del procesador utilizado, sino de la combinación del compilador, el SO y el modelo de subprocesos.
A falta de documentación que indique lo contrario, no asumiría que ninguna acción es segura para subprocesos, particularmente con procesadores multi-core (o sistemas de múltiples procesadores). Tampoco confiaría en las pruebas, ya que los problemas de sincronización de subprocesos probablemente 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.
En x86 / Windows en C / C ++, no debe suponer que es seguro para subprocesos. Debería usar InterlockedIncrement() y InterlockedDecrement() si necesita operaciones atómicas.
Incluso si se reduce a una única instrucción de ensamblaje, incrementando el valor directamente en la memoria, aún 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 escribir el valor actualizado, el segundo procesador lo lee. Después de que ambos procesadores escriben el valor nuevamente, se habrá incrementado solo una, no dos veces.
Hay una manera de evitar este problema; Los procesadores x86 (y la mayoría de los procesadores multi-core que usted 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 aparece atómica. Sin embargo, dado que esto es muy costoso, solo se realiza cuando lo solicita el código, generalmente en x86 a través del prefijo LOCK
. Otras arquitecturas pueden hacer esto de otras maneras, con resultados similares; por ejemplo, load-linked / store-conditional y atomic compare-and-swap (los procesadores x86 recientes también tienen este último).
Tenga en cuenta que usar volatile
no ayuda aquí; solo le dice al compilador que la variable puede haber sido modificada externamente y que las lecturas para esa variable no deben almacenarse en caché en un registro ni ser optimizadas. No hace que el compilador use primitivas atómicas.
La mejor forma es usar primitivas atómicas (si su compilador o bibliotecas las tienen), o hacer el incremento directamente en el ensamblaje (usando las instrucciones atómicas correctas).
No se puede hacer una declaración general sobre ++ i o i ++. ¿Por qué? Considere incrementar un 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 interrumpido por un cambio de contexto de hilo.
Además, ++i
no siempre es "agregar uno al valor". En un lenguaje como C, incrementar un puntero en realidad agrega el tamaño de la cosa apuntada. Es decir, si i
soy un puntero a una estructura de 32 bytes, ++i
agrega 32 bytes. Mientras que casi todas las plataformas tienen una instrucción de "incremento de valor en la dirección de memoria" que es atómica, no todas tienen una instrucción atómica "agregar valor arbitrario al valor en la dirección de memoria".
Nunca asuma que un incremento se compilará hasta una operación atómica. Use InterlockedIncrement o las funciones similares que existan en su plataforma de destino.
Editar: Acabo de buscar esta pregunta específica y el incremento en X86 es atómico en sistemas de procesador único, pero no en sistemas de multiprocesador. Usar el prefijo de bloqueo puede hacer que sea atómico, pero es mucho más portátil solo usar InterlockedIncrement.
Para un contador, recomiendo usar la expresión comparar y cambiar, que es tanto no bloqueable como segura para subprocesos.
Aquí está en Java:
public class IntCompareAndSwap {
private int value = 0;
public synchronized int get(){return value;}
public synchronized int compareAndSwap(int p_expectedValue, int p_newValue){
int oldValue = value;
if (oldValue == p_expectedValue)
value = p_newValue;
return oldValue;
}
}
public class IntCASCounter {
public IntCASCounter(){
m_value = new IntCompareAndSwap();
}
private IntCompareAndSwap m_value;
public int getValue(){return m_value.get();}
public void increment(){
int temp;
do {
temp = m_value.get();
} while (temp != m_value.compareAndSwap(temp, temp + 1));
}
public void decrement(){
int temp;
do {
temp = m_value.get();
} while (temp > 0 && temp != m_value.compareAndSwap(temp, temp - 1));
}
}
Si desea un incremento atómico en C ++, puede usar librerías C ++ 0x (el tipo de datos std::atomic
) o algo así como TBB.
Hubo una vez en que las pautas de codificación de GNU decían que actualizar los tipos de datos que se ajustaban a una palabra era "generalmente seguro", pero ese consejo es incorrecto para las máquinas SMP, incorrecto para algunas arquitecturas y erróneo 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 para que las escrituras solo lleven un ciclo, también ocurren simultáneamente, por lo que no se puede garantizar qué escritura tiene éxito. No obtendrá datos parcialmente actualizados, pero una escritura desaparecerá porque no hay otra forma de manejar este caso.
Compare-and-swap coordina adecuadamente entre múltiples CPU, pero no hay razón para creer que cada asignación de variable de tipos de datos de una sola palabra usará compare-and-swap.
Y aunque un compilador de optimización no afecta cómo se compila una carga / tienda, puede cambiar cuando ocurre la carga / almacenamiento, causando serios problemas si espera que sus lecturas y escrituras sucedan en el mismo orden en que aparecen en el código fuente ( el bloqueo de doble control más famoso no funciona en C ++ vainilla).
NOTA: Mi respuesta original también decía que la arquitectura Intel de 64 bits se rompió al tratar con datos de 64 bits. Eso no es cierto, así que edité la respuesta, pero mi edición decía que los chips PowerPC estaban rotos. Eso es cierto cuando se leen valores inmediatos (es decir, constantes) en los registros (consulte las dos secciones denominadas "Cargando punteros" en el listado 2 y el listado 4). Pero hay una instrucción para cargar datos de la memoria en un ciclo ( lmw
), así que lmw
esa parte de mi respuesta.
Si está compartiendo incluso una int entre subprocesos en un entorno multi-core, necesita las barreras de memoria adecuadas en su lugar. Esto puede significar el uso de instrucciones interconectadas (vea InterlockedIncrement en win32, por ejemplo), o el uso de un lenguaje (o compilador) que garantice ciertas garantías de subprocesos. Con instrucciones y reordenamientos de nivel de CPU y caches y otros problemas, a menos que tenga esas garantías, no asuma que algo compartido entre subprocesos es seguro.
Editar: Una cosa que puede suponerse 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 mezclaron. Si dos escrituras pasan una encima de la otra, una ganará y la otra será descartada. Si tiene cuidado, puede aprovechar esto y ver que +++ o i ++ son seguros para subprocesos en la situación de escritor único / lector múltiple.
Si su lenguaje de programación no dice nada acerca de los hilos, pero se ejecuta en una plataforma multiproceso, ¿cómo puede cualquier construcción de lenguaje ser seguro para subprocesos?
Como otros señalaron: debe proteger el acceso multiproceso a las variables mediante llamadas específicas de la plataforma.
Hay bibliotecas que resumen la especificidad de la plataforma, y el próximo estándar de C ++ ha adaptado su modelo de memoria para hacer frente a los hilos (y por lo tanto puede garantizar la seguridad del hilo).
Tirar en el almacenamiento local de hilo; no es atómico, pero entonces no importa.
Usted dice "es solo una instrucción, sería ininterrumpible por 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 hilos que acceden a la misma variable al mismo tiempo sin ningún cambio de contexto.
Sin conocer el idioma, la respuesta es poner a prueba el diablo.
Has escuchado mal. 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 absoluto en los estándares. De hecho, dado que el multi-threading no es parte de los estándares ISO C o C ++ (a) , no se puede considerar que nada sea seguro para subprocesos en base a lo que cree que se compilará.
Es bastante factible que ++i
pueda compilar a una secuencia arbitraria, como:
load r0,[i] ; load memory into reg 0
incr r0 ; increment reg 0
stor [i],r0 ; store reg 0 back to memory
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:
lock ; disable task switching (interrupts)
load r0,[i] ; load memory into reg 0
incr r0 ; increment reg 0
stor [i],r0 ; store reg 0 back to memory
unlock ; enable task switching (interrupts)
donde el lock
desactiva y unlock
habilita las interrupciones. Pero, incluso entonces, esto puede no ser seguro para subprocesos en una arquitectura que tenga más de una de estas CPU compartiendo memoria (el lock
solo puede deshabilitar las interrupciones para una CPU).
El lenguaje en sí (o las bibliotecas para él, si no está integrado en el idioma) 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
y pthread_mutex_lock()
(disponible para C / C ++ en algunos sistemas operativos) son lo que necesita observar (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 threading en las especificaciones del lenguaje, incluidos los tipos de datos atómicos (aunque ellos, y los hilos en general, son opcionales, al menos en C).