¿Qué se garantiza con C ++ std :: atomic en el nivel de programador?

9

He escuchado y leído varios artículos, charlas y preguntas sobre el stackoverflow std::atomic, y me gustaría asegurarme de haberlo entendido bien. Debido a que todavía estoy un poco confundido con la línea de caché, la visibilidad de las escrituras se debe a posibles retrasos en los protocolos de coherencia de caché MESI (o derivados), almacenar buffers, invalidar colas, etc.

Leí que x86 tiene un modelo de memoria más fuerte, y que si se retrasa la invalidación de la memoria caché, x86 puede revertir las operaciones iniciadas. Pero ahora estoy interesado solo en lo que debería asumir como programador de C ++, independientemente de la plataforma.

[T1: thread1 T2: thread2 V1: variable atómica compartida]

Entiendo que std :: atomic garantiza que,

(1) No se producen carreras de datos en una variable (gracias al acceso exclusivo a la línea de caché).

(2) Dependiendo de qué orden de memoria usamos, garantiza (con barreras) que se produce una coherencia secuencial (antes de una barrera, después de una barrera o ambas).

(3) Después de una escritura atómica (V1) en T1, un RMW atómico (V1) en T2 será coherente (su línea de caché se habrá actualizado con el valor escrito en T1).

Pero como se menciona en la cartilla de coherencia de caché ,

La implicación de todas estas cosas es que, por defecto, las cargas pueden obtener datos obsoletos (si una solicitud de invalidación correspondiente estaba en la cola de invalidación)

Entonces, ¿es correcto lo siguiente?

(4) std::atomicNO garantiza que T2 no leerá un valor 'rancio' en una lectura atómica (V) después de una escritura atómica (V) en T1.

Se pregunta si (4) es correcto: si la escritura atómica en T1 invalida la línea de caché sin importar el retraso, ¿por qué T2 está esperando que la invalidación sea efectiva cuando una operación RMW atómica pero no en una lectura atómica?

Pregunta si (4) está mal: ¿cuándo puede un hilo leer un valor 'rancio' y "es visible" en la ejecución, entonces?

Agradezco mucho tus respuestas

Actualización 1

Entonces parece que me equivoqué en (3) entonces. Imagine la siguiente intercalación, para un V1 inicial = 0:

T1: W(1)
T2:      R(0) M(++) W(1)

Aunque se garantiza que el RMW de T2 sucederá completamente después de W (1) en este caso, aún puede leer un valor 'rancio' (estaba equivocado). De acuerdo con esto, atomic no garantiza la coherencia completa de la memoria caché, solo la coherencia secuencial.

Actualización 2

(5) Ahora imagine este ejemplo (x = y = 0 y son atómicos):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

De acuerdo con lo que hemos hablado, ver el "mensaje" que se muestra en la pantalla no nos daría información más allá de que T2 se ejecutó después de T1. Entonces, cualquiera de las siguientes ejecuciones podría haber sucedido:

  • T1 <T3 <T2
  • T1 <T2 <T3 (donde T3 ve x = 1 pero no y = 1 todavía)

¿está bien?

(6) Si un hilo siempre puede leer valores "obsoletos", ¿qué sucedería si tomáramos el escenario típico de "publicación" pero en lugar de indicar que algunos datos están listos, hacemos lo contrario (eliminar los datos)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

donde T2 todavía estaría usando un ptr eliminado hasta que vea que is_enabled es falso.

(7) Además, el hecho de que los subprocesos puedan leer valores "obsoletos" significa que un mutex no se puede implementar con un solo derecho atómico sin bloqueo. Requeriría un mecanismo de sincronización entre hilos. ¿Requeriría un atómico bloqueable?

Albert Caldas
fuente

Respuestas:

3
  1. Sí, no hay carreras de datos.
  2. Sí, con los memory_ordervalores adecuados puede garantizar la coherencia secuencial
  3. Una lectura-modificación-escritura atómica siempre ocurrirá completamente antes o completamente después de una escritura atómica en la misma variable
  4. Sí, T2 puede leer un valor obsoleto de una variable después de una escritura atómica en T1

Las operaciones de lectura-modificación-escritura atómica se especifican de manera de garantizar su atomicidad. Si otro hilo pudiera escribir en el valor después de la lectura inicial y antes de la escritura de una operación RMW, entonces esa operación no sería atómica.

Los subprocesos siempre pueden leer valores obsoletos, excepto cuando sucede antes de garantizar el orden relativo .

Si una operación RMW lee un valor "obsoleto", entonces garantiza que la escritura que genera será visible antes de cualquier escritura de otros hilos que sobrescriban el valor que leyó.

Actualización por ejemplo

Si T1 escribe x=1y T2 lo hace x++, xinicialmente con 0, las opciones desde el punto de vista del almacenamiento de xson:

  1. La escritura de T1 es primero, por lo que T1 escribe x=1, luego T2 lee x==1, incrementa eso a 2 y vuelve a escribir x=2como una operación atómica única.

  2. La escritura de T1 es la segunda. T2 lee x==0, lo incrementa a 1, y vuelve a escribir x=1como una sola operación, luego T1 escribe x=1.

Sin embargo, siempre que no haya otros puntos de sincronización entre estos dos subprocesos, los subprocesos pueden continuar con las operaciones que no están en la memoria.

Por lo tanto, T1 puede emitir x=1, luego proceder con otras cosas, aunque T2 aún leerá x==0(y por lo tanto escribirá x=1).

Si hay otros puntos de sincronización, se hará evidente qué hilo se modificó xprimero, porque esos puntos de sincronización forzarán un orden.

Esto es más evidente si tiene un condicional en el valor leído de una operación RMW.

Actualización 2

  1. Si usa memory_order_seq_cst(el valor predeterminado) para todas las operaciones atómicas, no necesita preocuparse por este tipo de cosas. Desde el punto de vista del programa, si ve "msg", se ejecutó T1, luego T3, luego T2.

Si usa otros ordenamientos de memoria (especialmente memory_order_relaxed), entonces puede ver otros escenarios en su código.

  1. En este caso, tienes un error. Supongamos que la is_enabledbandera es verdadera, cuando T2 entra en su whilebucle, por lo que decide ejecutar el cuerpo. T1 ahora elimina los datos, y T2 luego elimina la defensa del puntero, que es un puntero colgante, y se produce un comportamiento indefinido . Los atómicos no ayudan ni obstaculizan de ninguna manera más allá de evitar la carrera de datos en la bandera.

  2. Usted puede aplicar un mutex con una sola variable atómica.

Anthony Williams
fuente
Muchas gracias @Anthony Wiliams por tu rápida respuesta. He actualizado mi pregunta con un ejemplo de RMW leyendo un valor "obsoleto". Mirando este ejemplo, ¿qué quiere decir con un orden relativo y que W (1) de T2 será visible antes de cualquier escritura? ¿Significa que una vez que T2 haya visto los cambios de T1 ya no leerá W (1) de T2?
Albert Caldas
Entonces, si "los subprocesos siempre pueden leer valores obsoletos", significa que la coherencia de la memoria caché nunca está garantizada (al menos en el nivel del programador c ++). ¿Podrías echar un vistazo a mi actualización2 por favor?
Albert Caldas
Ahora veo que debería haber prestado más atención al lenguaje y los modelos de memoria de hardware para comprender completamente todo eso, esa era la pieza que me faltaba. ¡muchas gracias!
Albert Caldas
1

Con respecto a (3), depende del orden de memoria utilizado. Si ambas, la tienda y la operación RMW usan std::memory_order_seq_cst, entonces ambas operaciones se ordenan de alguna manera, es decir, la tienda ocurre antes de la RMW o al revés. Si la tienda realiza un pedido antes de la RMW, se garantiza que la operación RMW "vea" el valor almacenado. Si la tienda se ordena después del RMW, sobrescribirá el valor escrito por la operación RMW.

Si utiliza órdenes de memoria más relajadas, las modificaciones se ordenarán de alguna manera (el orden de modificación de la variable), pero no tiene garantías de si el RMW "ve" el valor de la operación de almacenamiento, incluso si la operación RMW es el orden después de la escritura en el orden de modificación de la variable.

En caso de que quiera leer otro artículo más, puedo remitirlo a Modelos de memoria para programadores C / C ++ .

mpoeter
fuente
Gracias por el artículo, aún no lo había leído. Incluso si es bastante viejo, ha sido útil reunir mis ideas.
Albert Caldas
1
Me alegra escuchar eso: este artículo es un capítulo ligeramente extendido y revisado de mi tesis de maestría. :-) Se centra en el modelo de memoria como C ++ 11 introducido; Podría actualizarlo para reflejar los cambios (pequeños) introducidos en C ++ 14/17. ¡Avíseme si tiene algún comentario o sugerencia para mejorar!
mpoeter