¿Son atómicos los tipos fundamentales de C/C++?
multithreading atomic (4)
¿Qué es atómico?
Atómico, como describir algo con la propiedad de un átomo. La palabra átomo se origina del latín atomus significa "indiviso".
Por lo general, pienso en una operación atómica (independientemente del idioma) que tenga dos cualidades:
Una operación atómica es siempre indivisa.
Es decir, se realiza de forma indivisible, creo que esto es a lo que OP se refiere como "threadsafe". En cierto sentido, la operación ocurre instantáneamente cuando es vista por otro hilo.
Por ejemplo, es probable que la siguiente operación esté dividida (depende del compilador / hardware):
i += 1;
porque puede ser observado por otro hilo (en hardware y compilador hipotéticos) como:
load r1, i;
addi r1, #1;
store i, r1;
Dos hilos que realizan la operación anterior
i += 1
sin la sincronización adecuada pueden producir un resultado incorrecto.
Digamos que
i=0
inicialmente, el subproceso
T1
carga
T1.r1 = 0
y el subproceso
T2
carga
t2.r1 = 0
.
Ambos hilos incrementan sus respectivos
r1
s en 1 y luego almacenan el resultado en
i
.
Aunque se han realizado dos incrementos, el valor de
i
sigue siendo solo 1 porque la operación de incremento fue divisible.
Tenga en cuenta que si hubiera habido sincronización antes y después de
i+=1
el otro subproceso habría esperado hasta que se completara la operación y, por lo tanto, habría observado una operación indivisa.
Tenga en cuenta que incluso una simple escritura puede o no ser indivisa:
i = 3;
store i, #3;
dependiendo del compilador y hardware.
Por ejemplo, si la dirección de
i
no está alineada adecuadamente, entonces se debe utilizar una carga / almacén no alineado que la CPU ejecuta como varias cargas / almacenes más pequeños.
Una operación atómica ha garantizado la semántica de pedidos de memoria.
Las operaciones no atómicas pueden reordenarse y no necesariamente se producen en el orden escrito en el código fuente del programa.
Por ejemplo, bajo la
regla "como si",
el compilador puede reordenar las tiendas y las cargas según lo considere conveniente, siempre que todo el acceso a la memoria volátil ocurra en el orden especificado por el programa "como si" el programa fuera evaluado de acuerdo con la redacción de la norma.
Por lo tanto, las operaciones no atómicas se pueden reorganizar rompiendo cualquier suposición sobre el orden de ejecución en un programa multiproceso.
Es por eso que un uso aparentemente inocente de un
int
procesar como una variable de señalización en la programación de subprocesos múltiples se interrumpe, incluso si las escrituras y las lecturas pueden ser indivisibles, el orden puede interrumpir el programa dependiendo del compilador.
Una operación atómica impone el orden de las operaciones a su alrededor dependiendo de qué semántica de memoria se especifique.
Ver
std::memory_order
.
La CPU también puede reordenar sus accesos de memoria bajo las restricciones de ordenamiento de memoria de esa CPU. Puede encontrar las restricciones de ordenamiento de memoria para la arquitectura x86 en la sección 8.2 del Manual del desarrollador de software de las arquitecturas Intel 64 e IA32 a partir de la página 2212.
Los tipos primitivos (
int
,
char
, etc.) no son atómicos
Porque incluso si en ciertas condiciones pueden tener instrucciones indivisibles de almacenamiento y carga o incluso algunas instrucciones aritméticas, no garantizan el pedido de tiendas y cargas. Como tales, no son seguros para usar en contextos de subprocesos múltiples sin una sincronización adecuada para garantizar que el estado de memoria observado por otros subprocesos es lo que usted piensa que es en ese momento.
Espero que esto explique por qué los tipos primitivos no son atómicos.
¿Los tipos fundamentales de C / C ++, como
int
,
double
, etc., son atómicos, por ejemplo, threadsafe?
¿Están libres de carreras de datos? es decir, si un hilo escribe en un objeto de ese tipo mientras otro hilo lee, ¿está bien definido el comportamiento?
Si no, ¿depende del compilador u otra cosa?
Dado que C también se menciona (actualmente) en la pregunta a pesar de no estar en las etiquetas, el Estándar C dice:
5.1.2.3 Ejecución del programa
...
Cuando el procesamiento de la máquina abstracta se interrumpe al recibir una señal, los valores de los objetos que no son ni objetos atómicos libres de bloqueo ni de tipo
volatile sig_atomic_t
no se especifican, al igual que el estado del entorno de coma flotante. El valor de cualquier objeto modificado por el controlador que no sea un objeto atómico sin bloqueo ni de tipovolatile sig_atomic_t
vuelve indeterminado cuando el controlador sale, al igual que el estado del entorno de punto flotante si el controlador lo modifica y no se restaura a su estado original
y
5.1.2.4 Ejecuciones multiproceso y carreras de datos
...
Dos evaluaciones de expresión entran en conflicto si una de ellas modifica una ubicación de memoria y la otra lee o modifica la misma ubicación de memoria.
[varias páginas de estándares - algunos párrafos que abordan explícitamente los tipos atómicos]
La ejecución de un programa contiene una carrera de datos si contiene dos acciones en conflicto en diferentes subprocesos, al menos uno de los cuales no es atómico, y ninguno sucede antes que el otro. Cualquier carrera de datos de este tipo da como resultado un comportamiento indefinido.
Tenga en cuenta que los valores son "indeterminados" si una señal interrumpe el procesamiento, y el acceso simultáneo a tipos que no son explícitamente atómicos es un comportamiento indefinido.
No, los tipos de datos fundamentales (por ejemplo,
int
,
double
) no son atómicos, consulte
std::atomic
.
En su lugar, puede usar
std::atomic<int>
o
std::atomic<double>
.
Nota:
std::atomic
se introdujo con C ++ 11 y entiendo que antes de C ++ 11, el estándar C ++ no reconocía la existencia de subprocesos múltiples.
Como señaló @Josh,
std::atomic_flag
es un tipo atómico booleano.
Se
garantiza que no tendrá bloqueo
, a diferencia de las especializaciones
std::atomic
.
La documentación citada es de: http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4567.pdf . Estoy bastante seguro de que el estándar no es gratuito y, por lo tanto, esta no es la versión final / oficial.
1.10 Ejecuciones multiproceso y carreras de datos
- Dos evaluaciones de expresión entran en conflicto si una de ellas modifica una ubicación de memoria (1.7) y la otra lee o modifica la misma ubicación de memoria.
- La biblioteca define varias operaciones atómicas (Cláusula 29) y operaciones en mutexes (Cláusula 30) que se identifican especialmente como operaciones de sincronización. Estas operaciones juegan un papel especial al hacer que las asignaciones en un hilo sean visibles para otro. Una operación de sincronización en una o más ubicaciones de memoria es una operación de consumo, una operación de adquisición, una operación de liberación o una operación de adquisición y liberación. Una operación de sincronización sin una ubicación de memoria asociada es una cerca y puede ser una cerca de adquisición, una cerca de liberación o una cerca de adquisición y liberación. Además, hay operaciones atómicas relajadas, que no son operaciones de sincronización, y operaciones atómicas de lectura-modificación-escritura, que tienen características especiales.
- Dos acciones son potencialmente concurrentes si
(23.1) - son realizadas por diferentes hilos, o
(23.2) - no están secuenciados, y al menos uno lo realiza un manejador de señales.
La ejecución de un programa contiene una carrera de datos si contiene dos acciones conflictivas potencialmente concurrentes, al menos una de las cuales no es atómica, y ninguna ocurre antes que la otra, excepto por el caso especial para los manejadores de señales que se describen a continuación. Cualquier carrera de datos de este tipo da como resultado un comportamiento indefinido.
29.5 Tipos atómicos
- Habrá especializaciones explícitas de la plantilla atómica para los tipos integrales `` char, char
signed char
,unsigned char
,short
,unsigned short
,int
,unsigned int
,long
,unsigned long
,long long
,unsigned long long
,char16_
t,char32_t
,wchar_t
, y cualquier otro tipo que necesiten los typedefs en el encabezado<cstdint>
. Para cada integral de tipo integral, la especializaciónatomic<integral>
proporciona operaciones atómicas adicionales apropiadas para los tipos integrales. Habrá una especializaciónatomic<bool>
que proporciona las operaciones atómicas generales como se especifica en 29.6.1.
- Habrá especializaciones parciales de puntero de la plantilla de clase atómica. Estas especializaciones deben tener un diseño estándar, constructores triviales predeterminados y destructores triviales. Cada uno de ellos admitirá la sintaxis de inicialización agregada.
29.7 Tipo de bandera y operaciones
- Las operaciones en un objeto de tipo atomic_flag serán sin bloqueo. [Nota: Por lo tanto, las operaciones también deben estar libres de direcciones. Ningún otro tipo requiere operaciones sin bloqueo, por lo que el tipo atomic_flag es el tipo mínimo implementado en hardware necesario para cumplir con este estándar internacional. Los tipos restantes se pueden emular con atomic_flag, aunque con propiedades menos que ideales. - nota final]
Una información adicional que no he visto mencionada en las otras respuestas hasta ahora:
Si usa
std::atomic<bool>
, por ejemplo, y
bool
es realmente atómico en la arquitectura de destino, entonces el compilador no generará cercos o cercos redundantes.
Se generaría el mismo código que para un
bool
simple.
En otras palabras, el uso de
std::atomic
solo hace que el código sea menos eficiente si realmente es necesario para la corrección en la plataforma.
Entonces no hay razón para evitarlo.