Orden y visibilidad del modelo de memoria?


Traté de buscar detalles sobre esto, incluso leí el estándar sobre mutexes y atomics... pero aún así no podía entender las garantías de visibilidad del modelo de memoria C++11. Por lo que entiendo, la característica muy importante de mutex, ADEMÁS de la exclusión mutua, es garantizar la visibilidad. Aka no es suficiente que solo un hilo por tiempo aumente el contador, es importante que el hilo aumente el contador que fue almacenado por el hilo que fue el último uso del mutex (realmente no sé por qué la gente no menciona esto más cuando se habla de mutexes, tal vez tuve malos maestros :)). Por lo que puedo decir, atomic no impone visibilidad inmediata: (de la persona que mantiene boost::thread y ha implementado el subproceso c++11 y la biblioteca mutex):

Una cerca con memory_order_seq_cst no aplica inmediatamente visibilidad a otros hilos (y tampoco lo hace una instrucción MFENCE). Las restricciones de ordenación de memoria C++0x son solo eso - - - ordenar limitación. las operaciones memory_order_seq_cst forman una orden total, pero no hay restricciones sobre lo que es esa orden, excepto que debe estar de acuerdo con todos los hilos, y no debe violar otros pedidos limitación. En particular, los hilos pueden continuar viendo valores" obsoletos" durante algún tiempo, siempre que vean los valores en un orden consistente con limitación.

Y estoy de acuerdo con eso. Pero el problema es que tengo problemas para entender lo que C++11 construye con respecto a atómica son "global" y que solo garantizan la coherencia en las variables atómicas. En particular, tengo entendido cuál (si lo hay) de los siguientes pedidos de memoria garantizan que habrá una valla de memoria antes y después de la carga y las tiendas: http://www.stdthread.co.uk/doc/headers/atomic/memory_order.html

Por lo que puedo decir std::memory_order_seq_cst inserta la barrera mem mientras que otros solo imponen el orden de las operaciones en cierta ubicación de memoria.

Así que alguien puede aclarar esto hasta, supongo que mucha gente va a estar haciendo errores horribles usando std:: atomic, esp si no usan por defecto (std:: memory_order_seq_cst memory ordering)
2. si estoy en lo cierto significa que la segunda línea es redundante en este código:

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);  

3. las std::atomic_thread_fences tienen los mismos requisitos que los mutexes en un sentido que para asegurar la consistencia seq en vars no atómicos uno debe hacer std:: atomic_thread_fence(std:: memory_order_seq_cst); antes de la carga y std:: atomic_thread_fence (std::memory_order_seq_cst);
¿después de las tiendas?
4. Is

  {
    regularSum+=atomicVar.load();
    regularVar1++;
    regularVar2++;
    }
    //...
    {
    regularVar1++;
    regularVar2++;
    atomicVar.store(74656);
  }

Equivalente a

std::mutex mtx;
{
   std::unique_lock<std::mutex> ul(mtx);
   sum+=nowRegularVar;
   regularVar++;
   regularVar2++;
}
//..
{
   std::unique_lock<std::mutex> ul(mtx);
    regularVar1++;
    regularVar2++;
    nowRegularVar=(74656);
}

Creo que no, pero me gustaría estar seguro.

EDITAR: 5. ¿Puede afirmar el fuego?
Solo existen dos hilos.

atomic<int*> p=nullptr; 

El primer hilo escribe

{
    nonatomic_p=(int*) malloc(16*1024*sizeof(int));
    for(int i=0;i<16*1024;++i)
    nonatomic_p[i]=42;
    p=nonatomic;
}

El segundo hilo dice

{
    while (p==nullptr)
    {
    }
    assert(p[1234]==42);//1234-random idx in array
}
Author: Xeo, 2011-09-18

2 answers

Si te gusta tratar con cercas, entonces a.load(memory_order_acquire) es equivalente a a.load(memory_order_relaxed) seguido de atomic_thread_fence(memory_order_acquire). Del mismo modo, a.store(x,memory_order_release) es equivalente a una llamada a atomic_thread_fence(memory_order_release) antes de una llamada a a.store(x,memory_order_relaxed). memory_order_consume es un caso especial de memory_order_acquire, para datos dependientes solo. memory_order_seq_cst es especial, y forma una orden total en todas las operaciones memory_order_seq_cst. Mezclado con los demás es lo mismo que una adquisición para una carga, y una liberación para una tienda. memory_order_acq_rel es para operaciones de lectura-modificación-escritura, y es equivalente a una adquisición en la parte de lectura y una liberación en la parte de escritura de la RMW.

El uso de restricciones de orden en las operaciones atómicas puede o no resultar en instrucciones de valla reales, dependiendo de la arquitectura de hardware. En algunos casos, el compilador generará mejor código si pone la restricción de orden en la operación atómica en lugar de usar una valla separada.

En x86, las cargas siempre se adquieren, y las tiendas siempre se liberan. memory_order_seq_cst requiere un orden más fuerte con una instrucción MFENCE o una LOCK instrucción prefijada (hay una opción de implementación aquí en cuanto a si hacer que la tienda tenga el orden más fuerte o la carga). En consecuencia, las vallas de adquisición y liberación independientes no son ops, pero atomic_thread_fence(memory_order_seq_cst) no lo es (de nuevo requiere una instrucción ed MFENCE o LOCK).

Un efecto importante de las restricciones de orden es que ordenan otras operaciones.

std::atomic<bool> ready(false);
int i=0;

void thread_1()
{
    i=42;
    ready.store(true,memory_order_release);
}

void thread_2()
{
    while(!ready.load(memory_order_acquire)) std::this_thread::yield();
    assert(i==42);
}

thread_2 gira hasta que lee true de ready. Desde la tienda a {[21] {} en[23]} es un la liberación, y la carga es una adquisición, a continuación, la tienda sincroniza con la carga, y el de la tienda a i sucede-antes de la carga de i en la aserción, y la aserción no fuego.

2) La segunda línea en

atomicVar.store(42);
std::atomic_thread_fence(std::memory_order_seq_cst);  

Es, de hecho, potencialmente redundante, porque la tienda a atomicVar utiliza memory_order_seq_cst por defecto. Sin embargo, si hay otras operaciones atómicas no-memory_order_seq_cst en este hilo, entonces la valla puede tener consecuencias. Por ejemplo, actuaría como una valla de liberación para un a.store(x,memory_order_relaxed) posterior.

3) Las cercas y las operaciones atómicas no funcionan como mutexes. Puedes usarlos para construir mutexes, pero no funcionan como ellos. Usted no tiene que usar nunca atomic_thread_fence(memory_order_seq_cst). No hay ningún requisito de que las operaciones atómicas sean memory_order_seq_cst, y el orden en variables no atómicas se puede lograr sin, como en el ejemplo anterior.

4) No estos no son equivalentes. Su fragmento sin el bloqueo mutex es, por lo tanto, una carrera de datos e indefinida comportamiento.

5) No su afirmación no puede disparar. Con el orden de memoria predeterminado de memory_order_seq_cst, el almacenamiento y la carga desde el puntero atómico p funcionan como el almacenamiento y la carga en mi ejemplo anterior, y se garantiza que los almacenes en los elementos de la matriz ocurran-antes de las lecturas.

 23
Author: Anthony Williams,
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
2011-10-19 16:54:50

Por lo que puedo decir std::memory_order_seq_cst inserta la barrera mem mientras que otros solo imponen el orden de las operaciones en cierta ubicación de memoria.

Realmente depende de lo que estés haciendo y de la plataforma con la que estés trabajando. El modelo de orden de memoria fuerte en una plataforma como x86 creará un conjunto diferente de requisitos para la existencia de operaciones de valla de memoria en comparación con un modelo de orden más débil en plataformas como IA64, PowerPC, ARM, etc. Lo que el el parámetro predeterminado de std::memory_order_seq_cst es asegurar que, dependiendo de la plataforma, se utilizarán las instrucciones adecuadas de la cerca de memoria. En una plataforma como x86, no hay necesidad de una barrera de memoria completa a menos que esté haciendo una operación de lectura-modificación-escritura. Según el modelo de memoria x86, todas las cargas tienen semántica de adquisición de carga y todas las tiendas tienen semántica de liberación de tienda. Por lo tanto, en estos casos la enumeración std::memory_order_seq_cst básicamente crea un no-op ya que el modelo de memoria para x86 ya asegura que ese tipo de operaciones son consistentes a través de subprocesos, y por lo tanto no hay instrucciones de ensamblaje que implementen estos tipos de barreras de memoria parcial. Por lo tanto, la misma condición no-op sería verdadera si establece explícitamente un ajuste std::memory_order_release o std::memory_order_acquire en x86. Además, requerir una barrera de memoria completa en estas situaciones sería un impedimento de rendimiento innecesario. Como se ha señalado, sólo sería necesario para las operaciones de lectura-modificación-almacenamiento.

En otras plataformas con modelos de consistencia de memoria más débiles, sin embargo, ese no sería el caso, y por lo tanto usar std::memory_order_seq_cst emplearía las operaciones de cerco de memoria adecuadas sin que el usuario tenga que especificar explícitamente si desea una operación de carga-adquisición, almacenamiento-liberación o cerco de memoria completo. Estas plataformas tienen instrucciones específicas de la máquina para hacer cumplir tales contratos de consistencia de memoria, y la configuración std::memory_order_seq_cst resolvería el caso adecuado. Si el usuario desea llamar específicamente para una de estas operaciones que pueden a través de la explicit std::memory_order tipos de enumeración, pero no sería necesario ... el compilador resolvería la configuración correcta.

Supongo que mucha gente va a hacer errores horribles usando std:: atomic, esp si no usan por defecto (std::memory_order_seq_cst memory ordering)

Sí, si no saben lo que están haciendo, y no entienden qué tipos de semántica de barrera de memoria se requieren en ciertas operaciones, entonces se cometerán muchos errores si intentan indicar explícitamente el tipo de barrera de memoria y es la incorrecta, especialmente en plataformas que no va a ayudar a su mal entendimiento de la orden de memoria porque son más débiles en la naturaleza.

Finalmente, tenga en cuenta con su situación # 4 con respecto a un mutex que hay dos cosas diferentes que deben suceder aquí:

  1. No se debe permitir que el compilador reordene operaciones a través de la sección mutex y crítica (especialmente en el caso de un compilador de optimización)
  2. Debe haber las cercas de memoria necesarias creadas (dependiendo de la plataforma) que mantengan un estado donde todos los almacenes se completen antes de la sección crítica y la lectura de la variable mutex, y todos los almacenes se completen antes de salir de la sección crítica.

Dado que por defecto, los almacenes atómicos y las cargas se implementan con std::memory_order_seq_cst, entonces el uso de atomics también implementaría los mecanismos adecuados para satisfacer las condiciones #1 y #2. Dicho esto, en su primer ejemplo con atomics, la carga impondría la semántica de adquisición para el bloque, mientras que la tienda impondría la semántica de liberación. Sin embargo, no haría cumplir ningún orden particular dentro de la "sección crítica" entre estas dos operaciones. En su segundo ejemplo, tiene dos secciones diferentes con bloqueos, cada bloqueo tiene adquirir semántica. Dado que en algún momento tendría que liberar los bloqueos, que tendrían semántica de liberación, entonces no, los dos bloques de código no serían equivalentes. En el primer ejemplo, has creado una gran "sección crítica" entre la carga y la tienda (asumiendo que todo esto está sucediendo en el mismo hilo). En el segundo ejemplo tienes dos secciones críticas diferentes.

P.D. He encontrado el siguiente PDF particularmente instructivo, y usted también puede encontrarlo: http://www.nwcpp.org/Downloads/2008/Memory_Fences.pdf

 7
Author: Jason,
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
2011-09-19 14:57:49