¿Qué es exactamente std:: atomic?


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é significa exactamente hacer un objeto atómico? Por ejemplo, si hay dos subprocesos ejecutando simultáneamente el siguiente código:

a = a + 12;

Entonces, ¿toda la operación (digamos add_twelve_to(int)) es atómica? ¿O se hacen cambios a la variable atómica (so operator=())?

Author: Azeem, 2015-08-13

2 answers

Cada instanciación y especialización completa de std:: atomic representa un tipo, en el que diferentes hilos pueden operar simultáneamente (sus instancias), sin elevar el 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 que otro hilo lee de él, el comportamiento está bien definido.

Además, los accesos a objetos atómicos pueden establecer sincronización entre subprocesos y ordena los accesos a la memoria no atómica según lo especificado por std::memory_order.

std::atomic<> operaciones de wraps, que, en pre-C++ 11 veces, tuvieron que ser realizadas usando (por ejemplo) funciones entrelazadas con MSVC o bultins atómicos en el caso de GCC.

También, std::atomic<> le da más control al permitir varias órdenes de memoria , que especifican restricciones de sincronización y ordenación. Si quieres leer más sobre C++ 11 atomics and memory modelo, estos enlaces pueden ser útiles:

Tenga en cuenta que para los casos de uso típicos, probablemente usaría aritmética sobrecargada los operadores o 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 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 consistencia secuencial (total global ordering) entre todas las operaciones atómicas.

En algunos casos, sin embargo, esto puede no ser necesario (y nada viene gratis), por lo que es posible que desee utilizar más explícito formulario:

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, su ejemplo:

a = a + 12;

No evaluará a un solo op atómico: resultará en a.load() (que es atómico en sí mismo), luego suma entre este valor y 12 y a.store() (también atómico) del resultado final. Como he señalado anteriormente, std::memory_order_seq_cst se utilizará aquí.

Sin embargo, si escribes 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 su comentario:

Un regular int tiene cargas atómicas y almacenes. ¿Cuál es el punto de envolverlo con atomic<>?

Su declaración solo es verdadera para arquitecturas, que proporcionan tal 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 direcciones alineadas con word/dword para ser atómicas std::atomic<> es algo que se garantiza que sea atómico en cada plataforma, sin requisitos adicionales. Además, le permite escribe el código así:

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 esa 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 que while el bucle salga. Esto se debe a que:

  • store() a la bandera se realiza después de sharedData se establece (suponemos, que generateData() siempre devuelve algo útil, en particular, nunca devuelve NULL) y utiliza std::memory_order_release orden:

memory_order_release

Una operación de almacenamiento con este orden de memoria realiza la liberación operació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 while salga el bucle, y por lo tanto después de load() from flag devolverá un valor distinto de cero. load() usa std::memory_order_acquire orden:

std::memory_order_acquire

Una operación de carga con esta memoria order realiza la operación acquire en la ubicación de memoria afectada: no lee ni escribe en la el hilo se puede reordenar antes de esta carga. Todas las escrituras en otros hilos que liberan la misma variable atómica son visibles en la corriente thread .

Esto le da un control preciso sobre la sincronización y le permite especificar explícitamente cómo su código puede/puede no/se/no se comportará. Esto no sería posible si sólo la garantía era la atomicidad misma. Especialmente cuando se trata de modelos de sincronización muy interesantes como el release-consume ordering.

 76
Author: Mateusz Grzejek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-24 15:40:02

Entiendo que std::atomic<> hace un objeto atómico.

Eso es una cuestión de perspectiva... no se puede aplicar a objetos arbitrarios y hacer que sus operaciones se conviertan en atómicas, pero se pueden usar las especializaciones proporcionadas para (la mayoría) tipos integrales y punteros.

a = a + 12;

std::atomic<> no (usa expresiones de plantilla para) simplificar esto a una sola operación atómica, en su lugar el miembro operator T() const volatile noexcept hace un load() atómico de a, luego se agrega doce, y operator=(T t) noexcept hace un store(t).

 12
Author: Tony Delroy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-13 02:42:17