Entiendo que std::atomic<>
es un objeto atómico. ¿Pero atómico hasta qué punto? A mi entender, una operación puede ser atómica. ¿Qué se entiende exactamente por hacer un objeto atómico? Por ejemplo, si hay dos subprocesos que ejecutan simultáneamente el siguiente código:
a = a + 12;
Entonces, ¿toda la operación (por ejemplo add_twelve_to(int)
) es atómica? ¿O se realizan cambios en la variable atómica (so operator=()
)?
c++
multithreading
c++11
atomic
curioso
fuente
fuente
a.fetch_add(12)
si quieres un RMW atómico.std::atomic
permite que la biblioteca estándar decida qué se necesita para lograr la atomicidad.std::atomic<T>
es un tipo que permite operaciones atómicas. No mágicamente mejora tu vida, todavía tienes que saber qué quieres hacer con ella. Es para un caso de uso muy específico, y los usos de las operaciones atómicas (en el objeto) son generalmente muy sutiles y deben considerarse desde una perspectiva no local. Entonces, a menos que ya lo sepa y por qué desea operaciones atómicas, el tipo probablemente no sea de mucha utilidad para usted.Respuestas:
Cada instanciación y especialización completa de std :: atomic <> representa un tipo en el que diferentes subprocesos pueden operar simultáneamente (sus instancias), sin generar un comportamiento indefinido:
std::atomic<>
envuelve operaciones que, antes de C ++ 11 veces, tenían que realizarse utilizando (por ejemplo) funciones entrelazadas con MSVC o bultinas atómicas en el caso de CCG.Además,
std::atomic<>
le brinda más control al permitir varias órdenes de memoria que especifican restricciones de sincronización y orden. Si desea leer más sobre el modelo atómico y de memoria de C ++ 11, estos enlaces pueden ser útiles:Tenga en cuenta que, para casos de uso típicos, probablemente usaría operadores aritméticos sobrecargados u otro conjunto de ellos :
Debido a que la sintaxis del operador no le permite especificar el orden de la memoria, estas operaciones se realizarán con
std::memory_order_seq_cst
, ya que este es el orden predeterminado para todas las operaciones atómicas en C ++ 11. Garantiza la coherencia secuencial (orden global total) entre todas las operaciones atómicas.Sin embargo, en algunos casos, esto puede no ser necesario (y nada es gratis), por lo que es posible que desee utilizar una forma más explícita:
Ahora, tu ejemplo:
no evaluará a una única operación atómica: dará como resultado
a.load()
(que es atómico en sí mismo), luego la suma entre este valor12
ya.store()
(también atómico) del resultado final. Como señalé anteriormente,std::memory_order_seq_cst
se usará aquí.Sin embargo, si escribe
a += 12
, será una operación atómica (como señalé antes) y es aproximadamente equivalente aa.fetch_add(12, std::memory_order_seq_cst)
.En cuanto a tu comentario:
Su declaración solo es cierta para las arquitecturas que brindan dicha garantía de atomicidad para tiendas y / o cargas. Hay arquitecturas que no hacen esto. Además, generalmente se requiere que las operaciones se realicen en una dirección alineada palabra / palabra para que sea atómico,
std::atomic<>
es algo que se garantiza que sea atómico en todas las plataformas, sin requisitos adicionales. Además, te permite escribir código como este:Tenga en cuenta que la condición de aserción siempre será verdadera (y, por lo tanto, nunca se activará), por lo que siempre puede estar seguro de que los datos están listos después de las
while
salidas de bucle. Eso es porque:store()
al indicador se realiza después de quesharedData
se establece (suponemos quegenerateData()
siempre devuelve algo útil, en particular, nunca regresaNULL
) y utiliza elstd::memory_order_release
orden:sharedData
se usa después de laswhile
salidas de bucle y, por lo tanto, despuésload()
del indicador devolverá un valor distinto de cero.load()
usastd::memory_order_acquire
orden:Esto le brinda un control preciso sobre la sincronización y le permite especificar explícitamente cómo su código puede / no puede / no / se comportará. Esto no sería posible si la única garantía fuera la atomicidad misma. Especialmente cuando se trata de modelos de sincronización muy interesantes como el pedido de lanzamiento de consumo .
fuente
int
s?std::atomic
(std::memory_order
) sirven exactamente para limitar los reordenamientos que se permiten.Eso es una cuestión de perspectiva ... no puede aplicarlo a objetos arbitrarios y hacer que sus operaciones se vuelvan atómicas, pero se pueden utilizar las especializaciones proporcionadas para (la mayoría) tipos y punteros integrales.
std::atomic<>
no (utiliza expresiones de plantilla para) simplificar esto a una sola operación atómica, en su lugar, eloperator T() const volatile noexcept
miembro hace un atómicoload()
dea
, luego se agrega doce yoperator=(T t) noexcept
hace unstore(t)
.fuente
int
no garantiza de manera portátil que el cambio sea visible desde otros subprocesos, ni leerlo asegura que vea los cambios de otros subprocesos, y algunas cosas comomy_int += 3
no se garantiza que se realicen atómicamente a menos que lo usestd::atomic<>
, pueden involucrar una búsqueda, luego agregue, luego almacene la secuencia, en la que algún otro hilo que intente actualizar el mismo valor podría aparecer después de la búsqueda y antes de la tienda, y cerrar la actualización de su hilo.std::atomic
existe porque muchos ISA tienen soporte directo de hardware para elloLo que dice el estándar C ++ sobre
std::atomic
ha sido analizado en otras respuestas.Así que ahora veamos qué
std::atomic
compila para obtener un tipo diferente de información.La conclusión principal de este experimento es que las CPU modernas tienen soporte directo para operaciones de números enteros atómicos, por ejemplo, el prefijo LOCK en x86, y
std::atomic
básicamente existe como una interfaz portátil para esas instrucciones: ¿Qué significa la instrucción "lock" en el ensamblaje x86? En aarch64, se usaría LDADD .Este soporte permite alternativas más rápidas a métodos más generales como
std::mutex
, por ejemplo , que puede hacer que las secciones de múltiples instrucciones más complejas sean atómicas, a costa de ser más lento questd::atomic
porquestd::mutex
hacefutex
llamadas al sistema en Linux, que es mucho más lento que las instrucciones de usuario emitidas porstd::atomic
, vea también: ¿std :: mutex crea una cerca?Consideremos el siguiente programa de subprocesos múltiples que incrementa una variable global en varios subprocesos, con diferentes mecanismos de sincronización según la definición de preprocesador utilizada.
main.cpp
GitHub aguas arriba .
Compilar, ejecutar y desmontar:
Salida de condición de carrera extremadamente "incorrecta" para
main_fail.out
:y salida determinista "correcta" de los otros:
Desmontaje de
main_fail.out
:Desmontaje de
main_std_atomic.out
:Desmontaje de
main_lock.out
:Conclusiones:
la versión no atómica guarda el global en un registro e incrementa el registro.
Por lo tanto, al final, es muy probable que cuatro escrituras vuelvan a ser globales con el mismo valor "incorrecto"
100000
.std::atomic
compila alock addq
. El prefijo LOCK realiza la siguienteinc
búsqueda, modificación y actualización de memoria atómicamente.nuestro prefijo LOCK de ensamblaje en línea explícito se compila casi de la misma manera que
std::atomic
, excepto queinc
se usa nuestro en lugar deadd
. No estoy seguro de por qué eligió GCCadd
, teniendo en cuenta que nuestro INC generó una decodificación de 1 byte más pequeño.ARMv8 podría usar LDAXR + STLXR o LDADD en las CPU más nuevas: ¿Cómo inicio hilos en C simple?
Probado en Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51.
fuente