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 que un objeto sea atómico? Por ejemplo, si hay dos subprocesos que ejecutan simultáneamente el siguiente código:
a = a + 12;
Entonces, ¿toda la operación (digamos add_twelve_to(int)
) es atómica? ¿O se realizan cambios en la variable atómica (entonces operator=()
)?
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:
Los objetos de tipo atómico son los únicos objetos de C ++ que están libres de carreras de datos; es decir, si un hilo escribe en un objeto atómico mientras otro hilo lee de él, el comportamiento está bien definido.
Además, los accesos a los objetos atómicos pueden establecer una sincronización entre subprocesos y ordenar los accesos a la memoria no atómica según lo especificado por
std::memory_order
.
std::atomic<>
envuelve operaciones que, en tiempos anteriores a C ++ 11, tenían que realizarse utilizando (por ejemplo) funciones entrelazadas con MSVC o bultinas atómicas en el caso de GCC.
Además, std::atomic<>
le brinda más control al permitir varias órdenes de memoria que especifican restricciones de sincronización y ordenación. 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 :
std::atomic<long> value(0);
value++; //This is an atomic op
value += 5; //And so is this
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.
En algunos casos, sin embargo, es posible que esto no sea necesario (y nada es gratuito), por lo que es posible que desee utilizar una forma más explícita:
std::atomic<long> value {0};
value.fetch_add(1, std::memory_order_relaxed); // Atomic, but there are no synchronization or ordering constraints
value.fetch_add(5, std::memory_order_release); // Atomic, performs 'release' operation
Ahora, tu ejemplo:
a = a + 12;
no evaluará a una sola operación atómica: dará como resultado a.load()
(que es atómico en sí mismo), luego la suma entre este valor y 12
y a.store()
(también atómico) del resultado final. Como señalé anteriormente, std::memory_order_seq_cst
se utilizará aquí.
Sin embargo, si escribe a += 12
, será una operación atómica (como señalé antes) y es aproximadamente equivalente a a.fetch_add(12, std::memory_order_seq_cst)
.
En cuanto a tu comentario:
Un regular
int
tiene cargas atómicas y almacenes. ¿De qué sirve envolverloatomic<>
?
Su declaración solo es válida para 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 con palabra / palabra para que sea atómica, std::atomic<>
algo que se garantiza que será atómico en todas las plataformas, sin requisitos adicionales. Además, te permite escribir código como este:
void* sharedData = nullptr;
std::atomic<int> ready_flag = 0;
// Thread 1
void produce()
{
sharedData = generateData();
ready_flag.store(1, std::memory_order_release);
}
// Thread 2
void consume()
{
while (ready_flag.load(std::memory_order_acquire) == 0)
{
std::this_thread::yield();
}
assert(sharedData != nullptr); // will never trigger
processData(sharedData);
}
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 la while
salida del ciclo. Eso es porque:
store()
to the flag se realiza después de que sharedData
se establece (asumimos que generateData()
siempre devuelve algo útil, en particular, nunca regresa NULL
) y usa std::memory_order_release
order:
memory_order_release
Una operación de almacenamiento con esta orden de memoria realiza la operación de liberación : no se pueden reordenar lecturas o escrituras en el hilo actual después de esta tienda. Todas las escrituras en el hilo actual son visibles en otros hilos que adquieren la misma variable atómica
sharedData
se usa después de que el while
bucle sale y, por lo tanto, el load()
indicador de from devolverá un valor distinto de cero. load()
usa std::memory_order_acquire
orden:
std::memory_order_acquire
Una operación de carga con este orden de memoria realiza la operación de adquisición en la ubicación de memoria afectada: no se pueden reordenar lecturas o escrituras en el hilo actual antes de esta carga. Todas las escrituras en otros subprocesos que liberan la misma variable atómica son visibles en el subproceso actual .
Esto le da un control preciso sobre la sincronización y le permite especificar explícitamente cómo su código puede / no puede / no se comportará / 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 liberación-consumo .
Entiendo que eso
std::atomic<>
hace que un objeto sea atómico.
Eso es una cuestión de perspectiva ... no se puede aplicar a objetos arbitrarios y hacer que sus operaciones se vuelvan atómicas, pero se pueden usar las especializaciones proporcionadas para (la mayoría) de tipos y punteros integrales.
a = a + 12;
std::atomic<>
no (usa expresiones de plantilla para) simplificar esto a una sola operación atómica, en su lugar, el operator T() const volatile noexcept
miembro hace un atómico load()
de a
, luego se agrega doce y operator=(T t) noexcept
hace un store(t)
.
std::atomic
existe porque muchas ISA tienen soporte de hardware directo para él
Lo que dice el estándar C ++ std::atomic
se ha analizado en otras respuestas.
Así que ahora veamos qué se std::atomic
compila para obtener un tipo diferente de conocimiento.
La principal conclusión de este experimento es que las CPU modernas tienen soporte directo para operaciones de 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 "bloquear" 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
, que puede hacer que las secciones de múltiples instrucciones más complejas sean atómicas, a costa de ser más lento que std::atomic
porque std::mutex
hace futex
llamadas al sistema en Linux, que es mucho más lento que las instrucciones de usuario emitidas por std::atomic
, ver también: ¿Std :: mutex crea una cerca?
Consideremos el siguiente programa multiproceso que incrementa una variable global a través de múltiples subprocesos, con diferentes mecanismos de sincronización dependiendo de qué preprocesador se utilice.
main.cpp
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
size_t niters;
#if STD_ATOMIC
std::atomic_ulong global(0);
#else
uint64_t global = 0;
#endif
void threadMain() {
for (size_t i = 0; i < niters; ++i) {
#if LOCK
__asm__ __volatile__ (
"lock incq %0;"
: "+m" (global),
"+g" (i) // to prevent loop unrolling
:
:
);
#else
__asm__ __volatile__ (
""
: "+g" (i) // to prevent he loop from being optimized to a single add
: "g" (global)
:
);
global++;
#endif
}
}
int main(int argc, char **argv) {
size_t nthreads;
if (argc > 1) {
nthreads = std::stoull(argv[1], NULL, 0);
} else {
nthreads = 2;
}
if (argc > 2) {
niters = std::stoull(argv[2], NULL, 0);
} else {
niters = 10;
}
std::vector<std::thread> threads(nthreads);
for (size_t i = 0; i < nthreads; ++i)
threads[i] = std::thread(threadMain);
for (size_t i = 0; i < nthreads; ++i)
threads[i].join();
uint64_t expect = nthreads * niters;
std::cout << "expect " << expect << std::endl;
std::cout << "global " << global << std::endl;
}
Compilar, ejecutar y desensamblar:
comon="-ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic main.cpp -pthread"
g++ -o main_fail.out $common
g++ -o main_std_atomic.out -DSTD_ATOMIC $common
g++ -o main_lock.out -DLOCK $common
./main_fail.out 4 100000
./main_std_atomic.out 4 100000
./main_lock.out 4 100000
gdb -batch -ex "disassemble threadMain" main_fail.out
gdb -batch -ex "disassemble threadMain" main_std_atomic.out
gdb -batch -ex "disassemble threadMain" main_lock.out
Salida de condición de carrera "incorrecta" extremadamente probable para main_fail.out
:
expect 400000
global 100000
y salida determinista "correcta" de los demás:
expect 400000
global 400000
Desmontaje de main_fail.out
:
0x0000000000002780 <+0>: endbr64
0x0000000000002784 <+4>: mov 0x29b5(%rip),%rcx # 0x5140 <niters>
0x000000000000278b <+11>: test %rcx,%rcx
0x000000000000278e <+14>: je 0x27b4 <threadMain()+52>
0x0000000000002790 <+16>: mov 0x29a1(%rip),%rdx # 0x5138 <global>
0x0000000000002797 <+23>: xor %eax,%eax
0x0000000000002799 <+25>: nopl 0x0(%rax)
0x00000000000027a0 <+32>: add $0x1,%rax
0x00000000000027a4 <+36>: add $0x1,%rdx
0x00000000000027a8 <+40>: cmp %rcx,%rax
0x00000000000027ab <+43>: jb 0x27a0 <threadMain()+32>
0x00000000000027ad <+45>: mov %rdx,0x2984(%rip) # 0x5138 <global>
0x00000000000027b4 <+52>: retq
Desmontaje de main_std_atomic.out
:
0x0000000000002780 <+0>: endbr64
0x0000000000002784 <+4>: cmpq $0x0,0x29b4(%rip) # 0x5140 <niters>
0x000000000000278c <+12>: je 0x27a6 <threadMain()+38>
0x000000000000278e <+14>: xor %eax,%eax
0x0000000000002790 <+16>: lock addq $0x1,0x299f(%rip) # 0x5138 <global>
0x0000000000002799 <+25>: add $0x1,%rax
0x000000000000279d <+29>: cmp %rax,0x299c(%rip) # 0x5140 <niters>
0x00000000000027a4 <+36>: ja 0x2790 <threadMain()+16>
0x00000000000027a6 <+38>: retq
Desmontaje de main_lock.out
:
Dump of assembler code for function threadMain():
0x0000000000002780 <+0>: endbr64
0x0000000000002784 <+4>: cmpq $0x0,0x29b4(%rip) # 0x5140 <niters>
0x000000000000278c <+12>: je 0x27a5 <threadMain()+37>
0x000000000000278e <+14>: xor %eax,%eax
0x0000000000002790 <+16>: lock incq 0x29a0(%rip) # 0x5138 <global>
0x0000000000002798 <+24>: add $0x1,%rax
0x000000000000279c <+28>: cmp %rax,0x299d(%rip) # 0x5140 <niters>
0x00000000000027a3 <+35>: ja 0x2790 <threadMain()+16>
0x00000000000027a5 <+37>: retq
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" de 100000
.
std::atomic
compila a lock addq
. El prefijo LOCK hace lo siguiente: inc
buscar, modificar y actualizar la memoria de forma atómica.
nuestro prefijo LOCK de ensamblado en línea explícito se compila en casi lo mismo que std::atomic
, excepto que inc
se usa nuestro en lugar de add
. No estoy seguro de por qué eligió GCC add
, considerando que nuestro INC generó una decodificación 1 byte más pequeña.
ARMv8 podría usar LDAXR + STLXR o LDADD en CPU más nuevas: ¿Cómo inicio subprocesos en C simple?
Probado en Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51.
La estrella de HGTV, Christina Hall, revela que le diagnosticaron envenenamiento por mercurio y plomo, probablemente debido a su trabajo como manipuladora de casas.
Recientemente salió a la luz un informe policial que acusa a la estrella de 'Love Is Blind', Brennon, de violencia doméstica. Ahora, Brennon ha respondido a los reclamos.
Conozca cómo Wynonna Judd se dio cuenta de que ahora es la matriarca de la familia mientras organizaba la primera celebración de Acción de Gracias desde que murió su madre, Naomi Judd.
Descubra por qué un destacado experto en lenguaje corporal cree que es fácil trazar "tales paralelismos" entre la princesa Kate Middleton y la princesa Diana.
Los inodoros arrojan columnas de aerosol invisibles con cada descarga. ¿Como sabemos? La prueba fue capturada por láseres de alta potencia.
Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?
The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!
¿Sigue siendo efectivo ese lote de repelente de insectos que te quedó del verano pasado? Si es así, ¿por cuánto tiempo?
Tapas elásticas de silicona de Tomorrow's Kitchen, paquete de 12 | $14 | Amazonas | Código promocional 20OFFKINJALids son básicamente los calcetines de la cocina; siempre perdiéndose, dejando contenedores huérfanos que nunca podrán volver a cerrarse. Pero, ¿y si sus tapas pudieran estirarse y adaptarse a todos los recipientes, ollas, sartenes e incluso frutas en rodajas grandes que sobran? Nunca más tendrás que preocuparte por perder esa tapa tan específica.
Hemos pirateado algunas ciudades industriales en esta columna, como Los Ángeles y Las Vegas. Ahora es el momento de una ciudad militar-industrial-compleja.
Un minorista está enlatando su sección de tallas grandes. Pero no están tomando la categoría solo en línea o descontinuándola por completo.
Entiendo totalmente, completamente si tienes una relación difícil con los animales de peluche. Son lindos, tienen valor sentimental y es difícil separarse de ellos.
El equipo está a la espera de las medallas que ganó en los Juegos Olímpicos de Invierno de 2022 en Beijing, ya que se está resolviendo un caso de dopaje que involucra a la patinadora artística rusa Kamila Valieva.
Miles de compradores de Amazon recomiendan la funda de almohada de seda Mulberry, y está a la venta en este momento. La funda de almohada de seda viene en varios colores y ayuda a mantener el cabello suave y la piel clara. Compre las fundas de almohada de seda mientras tienen hasta un 46 por ciento de descuento en Amazon
El jueves se presentó una denuncia de delito menor amenazante agravado contra Joe Mixon.
El Departamento de Policía de Lafayette comenzó a investigar a un profesor de la Universidad de Purdue en diciembre después de recibir varias denuncias de un "hombre sospechoso que se acercaba a una mujer".
Al igual que el mundo que nos rodea, el lenguaje siempre está cambiando. Mientras que en eras anteriores los cambios en el idioma ocurrían durante años o incluso décadas, ahora pueden ocurrir en cuestión de días o incluso horas.
Estoy de vuelta por primera vez en seis años. No puedo decirte cuánto tiempo he estado esperando esto.
“And a river went out of Eden to water the garden, and from thence it was parted and became into four heads” Genesis 2:10. ? The heart is located in the middle of the thoracic cavity, pointing eastward.
Creo, un poco tarde en la vida, en dar oportunidades a la gente. Generosamente.