Anduvimos recabado en el mundo on line para regalarte la respuesta a tu dilema, si continúas con dudas déjanos tu pregunta y te respondemos con mucho gusto, porque estamos para ayudarte.
Solución:
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 la 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.
También, std::atomic<>
le da más control al permitir varias órdenes de memoria que especifican la sincronización y las restricciones de orden. Si desea leer más sobre el modelo atómico y de memoria de C ++ 11, estos enlaces pueden ser útiles:
- Ordenamiento atómico y de memoria en C ++
- Comparación: programación sin bloqueo con atomics en C ++ 11 frente a mutex y bloqueos RW
- C ++ 11 introdujo un modelo de memoria estandarizado. ¿Qué significa? ¿Y cómo afectará a la programación en C ++?
- Concurrencia en C ++ 11
Tenga en cuenta que, para casos de uso típicos, probablemente usaría operadores aritméticos sobrecargados u otro conjunto de ellos:
std::atomic 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 gratis), por lo que es posible que desee utilizar una forma más explícita:
std::atomic 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: resultará en 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 habitual
int
tiene cargas atómicas y almacenes. ¿Cuál es el punto de envolverlo conatomic<>
?
Tu declaración es solo true para arquitecturas que brinden 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 ser atómica. std::atomic<>
es algo que está garantizado para ser atómico en cada plataforma, sin requisitos adicionales. Además, te permite escribir código como este:
void* sharedData = nullptr;
std::atomic 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á true (y, por lo tanto, nunca se activará), por lo que siempre puede estar seguro de que los datos están listos después while
salidas de bucle. Eso es porque:
store()
a la bandera se realiza despuéssharedData
está establecido (asumimos quegenerateData()
siempre devuelve algo útil, en particular, nunca devuelveNULL
) y usosstd::memory_order_release
pedido:
memory_order_release
Una operación de almacenamiento con esta orden de memoria realiza el liberación
operación: no se pueden reordenar lecturas o escrituras en el hilo actual
después 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 dewhile
salidas de bucle, y por lo tanto después deload()
from flag devolverá un valor distinto de cero.load()
usosstd::memory_order_acquire
pedido:
std::memory_order_acquire
Una operación de carga con este orden de memoria realiza el adquirir operació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
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 suma doce, y operator=(T t) noexcept
hace un store(t)
.
std::atomic
existe porque muchas ISA tienen soporte de hardware directo para ello
Lo que dice el estándar C ++ sobre std::atomic
ha sido analizado en otras respuestas.
Así que ahora veamos qué std::atomic
se compila para obtener un tipo diferente de información.
La principal conclusión de este experimento es que las CPU modernas tienen soporte directo para operaciones de enteros atómicos, por ejemplo, LOCK prefix 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 lentas 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
, vea también: ¿std :: mutex crea una cerca?
Consideremos el siguiente programa de múltiples subprocesos que incrementa una variable global a través de múltiples subprocesos, con diferentes mecanismos de sincronización según la definición de preprocesador que se utilice.
main.cpp
#include
#include
#include
#include
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 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;
GitHub en sentido ascendente.
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
0x000000000000278b <+11>: test %rcx,%rcx
0x000000000000278e <+14>: je 0x27b4
0x0000000000002790 <+16>: mov 0x29a1(%rip),%rdx # 0x5138
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
0x00000000000027ad <+45>: mov %rdx,0x2984(%rip) # 0x5138
0x00000000000027b4 <+52>: retq
Desmontaje de main_std_atomic.out
:
0x0000000000002780 <+0>: endbr64
0x0000000000002784 <+4>: cmpq $0x0,0x29b4(%rip) # 0x5140
0x000000000000278c <+12>: je 0x27a6
0x000000000000278e <+14>: xor %eax,%eax
0x0000000000002790 <+16>: lock addq $0x1,0x299f(%rip) # 0x5138
0x0000000000002799 <+25>: add $0x1,%rax
0x000000000000279d <+29>: cmp %rax,0x299c(%rip) # 0x5140
0x00000000000027a4 <+36>: ja 0x2790
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
0x000000000000278c <+12>: je 0x27a5
0x000000000000278e <+14>: xor %eax,%eax
0x0000000000002790 <+16>: lock incq 0x29a0(%rip) # 0x5138
0x0000000000002798 <+24>: add $0x1,%rax
0x000000000000279c <+28>: cmp %rax,0x299d(%rip) # 0x5140
0x00000000000027a3 <+35>: ja 0x2790
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 regresen a global con el mismo valor "incorrecto" de
100000
. -
std::atomic
compila alock addq
. La cerradura prefix hace lo siguienteinc
buscar, modificar y actualizar la memoria de forma atómica. -
nuestro BLOQUEO explícito del ensamblaje en línea prefix compila casi lo mismo que
std::atomic
, excepto que nuestroinc
se usa en lugar deadd
. No estoy seguro de por qué GCC eligióadd
, considerando que nuestro INC generó una decodificación 1 byte menor.
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.
Eres capaz de añadir valor a nuestra información asistiendo con tu veteranía en las interpretaciones.