Cppreference.com da el siguiente ejemplo para el uso de std :: memory_order_relaxed. ( https://en.cppreference.com/w/cpp/atomic/memory_order)

#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
 
std::atomic<int> cnt = {0};
 
void f()
{
    for (int n = 0; n < 1000; ++n) {
        cnt.fetch_add(1, std::memory_order_relaxed);
    }
}
 
int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n) {
        v.emplace_back(f);
    }
    for (auto& t : v) {
        t.join();
    }
    std::cout << "Final counter value is " << cnt << '\n';
}

Salida: el valor final del contador es 10000

¿Es este un ejemplo correcto / sólido (¿Puede un compilador de quejas estándar introducir optimizaciones que producirán diferentes respuestas?). Dado que std :: memory_order_relaxed solo garantiza que la operación sea atómica, un subproceso puede no ver una actualización de otro subproceso. ¿Me estoy perdiendo de algo?

1
Uthpala Wettewa 25 jun. 2020 a las 08:18

2 respuestas

Sí, este es un ejemplo correcto, así que no, un compilador no puede introducir optimizaciones que arrojarían un resultado diferente. Tiene razón en que, en general, no se garantiza que un hilo vea una actualización de otro hilo (o más específicamente, no hay garantía cuando tal actualización se hace visible). Sin embargo, en este caso, cnt se actualiza utilizando una operación de lectura-modificación-escritura atómica, y los estados estándar en [atomics.order]:

Las operaciones de lectura-modificación-escritura atómica siempre leerán el último valor (en el orden de modificación) escrito antes de la escritura asociada con la operación de lectura-modificación-escritura.

Y esto tiene absolutamente sentido si lo piensas, porque de lo contrario no sería posible hacer una operación atómica de lectura-modificación-escritura. Supongamos que fetch_add no vería la última actualización, sino algún valor anterior. Eso significaría que la operación incrementaría ese valor anterior y lo almacenaría. Pero eso implicaría que 1) los valores devueltos por fetch_add no están aumentando estrictamente (algunos subprocesos verían el mismo valor) y 2) que se pierden algunas actualizaciones.

0
mpoeter 25 jun. 2020 a las 08:53

La pista de por qué esto funciona se puede encontrar en la primera oración de la descripción en la página has vinculado (el énfasis es mío):

std::memory_order especifica cómo accede a la memoria, incluidos los regulares, los accesos de memoria no atómica se deben ordenar alrededor de un atómico operación.

Observe cómo esto no se refiere al acceso a la memoria en el atómico en sí, sino a los accesos a la memoria que rodean al atómico. Los accesos concurrentes a un solo atómico siempre tienen requisitos estrictos de orden, de lo contrario sería imposible razonar sobre su comportamiento en primer lugar.

En el caso del contador, obtendrá la garantía de que fetch_add se comportará más o menos como se esperaba: el contador aumenta uno a la vez, no se omiten valores y no se contarán dos veces. Puede verificar esto fácilmente inspeccionando los valores de retorno de las llamadas individuales fetch_add. Obtiene esas garantías siempre, independientemente del pedido de memoria.

Las cosas se ponen interesantes tan pronto como asigna un significado a esos valores de contador en el contexto de la lógica del programa circundante. Por ejemplo, podría utilizar un cierto valor de contador para indicar que un paso de cálculo anterior ha puesto a disposición una determinada pieza de datos. Esto requerirá pedidos de memoria, si esa relación entre el contador y los datos necesita persistir a través de subprocesos: con el pedido relajado, en el punto donde observa el valor del contador que está esperando, no tiene garantía de que los datos que está esperando para está listo también. Incluso si el contador se establece después de que los datos hayan sido escritos por el hilo productor, este orden de las operaciones de memoria no se traduce a través de los límites del hilo. Deberá especificar un orden de memoria que ordene la escritura en los datos con respecto al cambio del contador en los subprocesos. Lo crucial que hay que entender aquí es que si bien se garantiza que las operaciones sucederán en un cierto orden dentro de un hilo, ese orden ya no está garantizado cuando se observan los mismos datos de un hilo diferente.

Entonces, la regla general es: si solo está manipulando un átomo de forma aislada, no necesita ningún pedido. Tan pronto como esa manipulación se interprete en el contexto de otros accesos de memoria no relacionados (¡incluso si esos accesos son en sí mismos atómicos!), Debe preocuparse por usar el orden correcto.

El consejo habitual aplica que, a menos que tenga realmente, realmente, realmente buenas razones para hacerlo, debe seguir con el valor predeterminado memory_order_seq_cst. Como desarrollador de aplicaciones, no querrá meterse con los pedidos de memoria a menos que tenga evidencia empírica fuerte de que vale la pena el problema con el que sin duda se encontrará.

0
ComicSansMS 25 jun. 2020 a las 08:51