c++ - salida 10 con Memory_order_seq_cst

CorePress2024-01-25  9

Cuando ejecuto este programa obtengo un resultado de 10, lo que me parece imposible. Estoy ejecutando esto en x86_64 core i3 ubuntu.

Si el resultado es 10, entonces 1 debe provenir de c o d.

También en el hilo t[0], asignamos c como 1. Ahora a es 1 ya que ocurre antes de c=1. c es igual a b que fue establecido en 1 por el subproceso 1. Entonces, cuando almacenamos d debería ser 1 como a=1.

¿Puede ocurrir la salida 10 con Memory_order_seq_cst? Intenté insertar un atomic_thread_fence(seq_cst) en ambos subprocesos entre la primera (variable =1) y la segunda línea (printf) pero todavía no funcionó.

Descomentar ambas vallas no funciona. Intenté ejecutar con g++ y clang++. Ambos dan el mismo resultado.

#include<thread>
#include<unistd.h>
#include<cstdio>
#include<atomic>
using namespace std;

atomic<int> a,b,c,d;

void foo(){
        a.store(1,memory_order_seq_cst);
//        atomic_thread_fence(memory_order_seq_cst);
        c.store(b,memory_order_seq_cst);
}

void bar(){
        b.store(1,memory_order_seq_cst);
  //      atomic_thread_fence(memory_order_seq_cst);
        d.store(a,memory_order_seq_cst);
}

int main(){
        thread t[2];
        t[0]=thread(foo); t[1]=thread(bar);
        t[0].join();t[1].join();
        printf("%d%d\n",c.load(memory_order_seq_cst),d.load(memory_order_seq_cst));
}
bash$ while [ true ]; do ./a.out | grep "10" ; done 
10
10
10
10

¿Depuraste/estableciste puntos de interrupción y pasos únicos?

-Esturas

28/03/2021 a las 10:11

1

@StureS es una pregunta de subprocesos, establecer puntos de interrupción y realizar cambios por pasos cambia el comportamiento del programa.

- Richard Critten

28/03/2021 a las 10:14

1

Eso debería darte una idea de dónde está el problema. Vea la respuesta de Dani a continuación.

-Esturas

28/03/2021 a las 11:07

2

@RichardCritten: Para ser justos, establecer puntos de interrupción y realizar un paso único en cada hilo por separado puede permitirle explorar posibles ordenamientos. Pero no reordenar el tiempo de ejecuciónefectos de los buffers de almacenamiento y múltiples solicitudes en vuelo; El paso único es tan lento que cada operación asm es efectivamente seq_cst, después de que el compilador haya determinado el orden en tiempo de compilación que eligió. Puede ser una técnica útil para comprobar la cordura de un diseño en busca de algo como una cola sin bloqueo, pero probablemente no para algo como esto. (Aunque todo ya es seq_cst así que tal vez)

-Peter Cordes

28/03/2021 a las 14:24

1

@nvn: descripción de tu textoription todavía habla de printfs dentro de cada función de hilo, no después de que ambos se unan. Cualquier forma sería equivalente si hubiera impreso d=%d\n o lo que sea; de esta manera no queda ambiguo qué número proviene de qué var.

-Peter Cordes

28/03/2021 a las 14:38



------------------------------------

10 (c=1, d=0) se explica fácilmente: bar se ejecutó primero y terminó antes de que foo leyera b.

Las peculiaridades de la comunicación entre núcleos para iniciar subprocesos en diferentes núcleos significan que es fácilmente posible que esto suceda incluso si el subproceso (foo) se ejecutó primero en el subproceso principal. p.ej. tal vez llegó una interrupción al núcleo que el sistema operativo eligiófoo, retrasando su entrada en ese código1.

Recuerde que seq_cst solo garantiza que exista algún orden total para todas las operaciones seq_cst que sea compatible con el orden secuenciado antes dentro de cada hilo. (Y cualquier otra ocurre -antes que la relación establecida por otros factores). Por lo tanto, el siguiente orden de operaciones atómicas es posible sin siquiera separar a.load2 in bar por separado del d.store del int temporal resultante.

        b.store(1,memory_order_seq_cst);   // bar1.  b=1
        d.store(a,memory_order_seq_cst);   // bar2.  a.load reads 0, d=0

        a.store(1,memory_order_seq_cst);   // foo1
        c.store(b,memory_order_seq_cst);   // foo2.  b.load reads 1, c=1
// final: c=1, d=0

atomic_thread_fence(seq_cst) no tiene ningún impacto en ninguna parte porque todas sus operaciones ya son seq_cst. Básicamente, una valla simplemente deja de reordenar las operaciones de este hilo; no espera ni se sincroniza con vallas en otros hilos.

(Solo una carga que ve un valor almacenado por otro hilo puede crear sincronización.Pero tal carga no espera a la otra tienda; No tiene forma de saber que hay otra tienda. Si desea seguir cargando hasta que vea el valor que espera, debe escribir un ciclo de giro y espera).

Nota a pie de página 1: Dado que todas sus variables atómicas probablemente estén en la misma línea de caché, incluso si la ejecución alcanzó la parte superior de foo y bar al mismo tiempo en dos núcleos diferentes, el intercambio falso probablemente permitirá que ambas operaciones de un hilo se realicen mientras que el otro Core todavía está esperando obtener la propiedad exclusiva. Aunque las tiendas seq_cst son lo suficientemente lentas (al menos en x86) como para que las cuestiones de equidad del hardware puedan renunciar a la propiedad exclusiva después de confirmar la primera tienda de 1. De todos modos, hay muchas maneras de que ambas operaciones en un subproceso sucedan antes que el otro subproceso y obtengan 10 o 01. Incluso posEs posible obtener 11 si obtenemos b=1 y luego a=1 antes de cualquier carga. El uso de seq_cst evita que el hardware realice la carga antes de tiempo (antes de que la tienda sea visible globalmente), por lo que es muy posible.

Nota a pie de página 2: La evaluación lvalue-to-rvalue de bare a utiliza la conversión sobrecargada (int) que es equivalente a a.load(seq_cst). Las operaciones de foo podrían ocurrir entre esa carga y d.store que obtiene un valor temporal de ella. d.store(a) no es una copia atómica; es equivalente a int tmp = a; d.store(tmp);. Eso no es necesario para explicar tus observaciones.

3

a,b,c,d son todos atómicos. Si d.store(a) no es una copia atómica, ¿qué debo usar para hacerla atómica? ¿Puedes ser un poco detallado?

-nvn

30 de marzo de 2021 a las 11:33

1

@nvn: Lo que tienes es una carga atómica y un almacén atómico separado. C++11 (y la mayoría del hardware) no tiene instrucciones atómicas de 2 operandos, por lo que no puede convertirlo en una copia atómica, excepto mediante el uso de un bloqueo/mutex para no permitir que ningún otro hilo observe n.on-atomicidad. O en algunos ISA, como algunas versiones de m68k, hay soporte de hardware para una operación como DCAS que es un RMW atómico en 2 operaciones de memoria no contiguas a la vez, pero C++ no lo expone; de ​​lo contrario, casi todas las implementaciones necesitaría .is_lock_free() == false.

-Peter Cordes

30 de marzo de 2021 a las 12:23

1

@nvn: O en algunas CPU modernas, puede usar la memoria transaccional para realizar una lectura + escritura en una sola transacción. (por ejemplo, Intel TSX si no está deshabilitado por motivos de seguridad (canales laterales de fuga de información) por motivos de erratas). Por supuesto, en la mayoría de los casos de uso no hay necesidad de esto, y la mayoría de los observadores no podrían decirlo. la diferencia entre carga y almacenamiento atómicos por separado frente a una sola transacción atómica.

-Peter Cordes

30 de marzo de 2021 a las 12:27



------------------------------------

Las sentencias printf no están sincronizadas, por lo que la salida 10 puede ser simplemente un 01 reordenado. 01 ocurre cuando las funciones anteriores a printf se ejecutan en serie.

1

No creo que se deba a printf. Así que eliminé printf y mire el programa actualizado donde almaceno los valores en variables atómicas y los imprimo después de unir ambos hilos.

-nvn

28/03/2021 a las 11:28

Su guía para un futuro mejor - libreflare
Su guía para un futuro mejor - libreflare