c++ c++11 mutex atomic memory-barriers

c++ - ¿Modelo de memoria ordenando y visibilidad?



c++11 mutex (2)

Intenté buscar detalles sobre esto, incluso leí el estándar sobre exclusión mutua y atómica ... pero todavía no pude entender las garantías de visibilidad del modelo de memoria C ++ 11. Por lo que entiendo, la característica muy importante de mutex BESIDE exclusión mutua es garantizar la visibilidad. Aka no es suficiente que solo un hilo por vez aumente el contador, es importante que el hilo aumente el contador que fue almacenado por el hilo que usó el mutex por última vez (realmente no sé por qué la gente no menciona esto más al discutir exclusión, tal vez tuve malos maestros :)). Entonces, por lo que puedo decir, atomic no impone una visibilidad inmediata: (de la persona que mantiene boost :: thread y ha implementado el subproceso c ++ 11 y la biblioteca de exclusión mutua):

Una valla con memory_order_seq_cst no impone la visibilidad inmediata a otros subprocesos (y tampoco lo hace una instrucción MFENCE). Las restricciones de orden de memoria de C ++ 0x son solo eso --- restricciones de orden. Las operaciones memory_order_seq_cst forman un orden total, pero no hay restricciones en lo que es ese pedido, excepto que debe ser acordado por todos los subprocesos, y no debe violar otras restricciones de orden. En particular, los hilos pueden seguir viendo valores "obsoletos" por algún tiempo, siempre que vean los valores en un orden consistente con las restricciones.

Y estoy bien con eso. Pero el problema es que tengo problemas para entender qué construcciones de C ++ 11 con respecto a atómicas son "globales" y que solo aseguran la coherencia en las variables atómicas. En particular, entiendo cuál (si alguno) de los siguientes pedidos de memoria garantiza 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 hacen cumplir el orden de las operaciones en cierta ubicación de memoria.

Entonces, ¿alguien puede aclarar esto? Supongo que mucha gente hará errores horribles usando std :: atomic, especialmente si no usan el valor predeterminado (std :: memory_order_seq_cst pedido de memoria)
2. Si tengo razón, 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. ¿std :: atomic_thread_fences tiene los mismos requisitos que los mutex en el sentido de que para garantizar la coherencia de las secuencias no atómicas, std :: atomic_thread_fence (std :: memory_order_seq_cst); antes de cargar y std :: atomic_thread_fence (std :: memory_order_seq_cst);
después de las tiendas?
4. es

{ 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 hacer valer fuego?
Solo existen dos hilos.

atomic<int*> p=nullptr;

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 lee

{ while (p==nullptr) { } assert(p[1234]==42);//1234-random idx in array }


Por lo que puedo decir, std :: memory_order_seq_cst inserta la barrera mem, mientras que otros solo hacen cumplir 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 fuerte modelo de pedido de memoria en una plataforma como x86 creará un conjunto diferente de requisitos para la existencia de operaciones de cerco de memoria en comparación con un modelo de pedido más débil en plataformas como IA64, PowerPC, ARM, etc. ¿Cuál es el parámetro predeterminado de std::memory_order_seq_cst es asegurarse de que, dependiendo de la plataforma, se utilizarán las instrucciones adecuadas para el cerco de memoria. En una plataforma como x86, no hay necesidad de una barrera de memoria completa a menos que esté realizando una operación de lectura-modificación-escritura. Según el modelo de memoria x86, todas las cargas tienen semántica de carga y adquisición, y todas las tiendas tienen semántica de lanzamiento 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 garantiza que esos tipos de operaciones sean coherentes en todos los subprocesos, y por lo tanto no hay instrucciones de ensamblaje que implementen estos tipos de barreras de memoria parciales . Por lo tanto, la misma condición de no std::memory_order_release sería verdadera si establece explícitamente una configuración 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 indicó, solo se requeriría para operaciones de lectura-modificación-tienda.

Sin embargo, en otras plataformas con modelos de consistencia de memoria más débiles, ese no sería el caso y, por lo tanto, el uso de std::memory_order_seq_cst emplearía las operaciones de cercado de memoria adecuadas sin que el usuario tenga que especificar explícitamente si desea una carga de adquisición, liberación de tienda , o funcionamiento de la valla de memoria completa. Estas plataformas tienen instrucciones específicas de la máquina para hacer cumplir dichos contratos de consistencia de memoria, y la configuración std::memory_order_seq_cst funcionaría en el caso adecuado. Si el usuario quisiera solicitar específicamente una de estas operaciones, puede std::memory_order través de los tipos explícitos de enumeración std::memory_order , pero no sería necesario ... el compilador elaborará la configuración correcta.

Supongo que mucha gente hará errores horribles usando std :: atomic, especialmente si no usan el valor predeterminado (std :: memory_order_seq_cst orden de memoria)

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

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

  1. No se debe permitir que el compilador reordene las operaciones a través del mutex y la sección crítica (especialmente en el caso de un compilador de optimización)
  2. Deben crearse las vallas de memoria necesarias (según la plataforma) que mantengan un estado en el que todas las tiendas se completen antes de la sección crítica y la lectura de la variable de exclusión mutua, y todas las tiendas se completen antes de salir de la sección crítica.

Dado que, de forma predeterminada, las tiendas atómicas y las cargas se implementan con std::memory_order_seq_cst , el uso de atómica 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 aplicaría la semántica de liberación. Sin embargo, no impondría ningún orden en particular dentro de la "sección crítica" entre estas dos operaciones. En su segundo ejemplo, tiene dos secciones diferentes con bloqueos, cada uno de los cuales ha adquirido una semántica. Ya que en algún momento tendrías que liberar los bloqueos, que tendrían una semántica de liberación, entonces no, los dos bloques de código no serían equivalentes. En el primer ejemplo, ha creado una gran "sección crítica" entre la carga y el almacén (suponiendo que todo esto suceda en el mismo hilo). En el segundo ejemplo tienes dos secciones críticas diferentes.

PD He encontrado el siguiente PDF particularmente instructivo, y puede que también lo encuentre: http://www.nwcpp.org/Downloads/2008/Memory_Fences.pdf


Si te gusta lidiar con cercas, a.load(memory_order_acquire) es equivalente a a.load(memory_order_relaxed) seguido de atomic_thread_fence(memory_order_acquire) . De manera similar, 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 , solo para datos dependientes. memory_order_seq_cst es especial, y forma un orden total en todas memory_order_seq_cst operaciones memory_order_seq_cst . Mezclado con los otros, es lo mismo que una adquisición para una carga y una versión para una tienda. memory_order_acq_rel es para memory_order_acq_rel de lectura-modificación-escritura, y es equivalente a una adquisición en la parte de lectura y una versión en la parte de escritura de la RMW.

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

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

Un efecto importante de las restricciones de ordenamiento 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 se lee true desde ready . Dado que la tienda que está ready en thread_1 es una versión, y la carga es una adquisición, entonces la tienda se sincroniza con la carga y la tienda con i ocurre antes de la carga de i en la aserción, y la aserción no se activará.

2) La segunda línea en

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

es de hecho potencialmente redundante, porque el almacén de atomicVar usa memory_order_seq_cst de forma predeterminada. Sin embargo, si hay otras operaciones atómicas que no son memory_order_seq_cst en este hilo, entonces la valla puede tener consecuencias. Por ejemplo, actuaría como una a.store(x,memory_order_relaxed) liberación para una posterior a.store(x,memory_order_relaxed) .

3) Las cercas y las operaciones atómicas no funcionan como mutexes. Puedes usarlos para construir mutex, pero no funcionan como ellos. 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 ordenamiento en variables no atómicas se puede lograr sin él, como en el ejemplo anterior.

4) No estos no son equivalentes. Su fragmento de código sin el bloqueo mutex es, por lo tanto, una carrera de datos y un comportamiento indefinido.

5) No, tu 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 sucederán antes de las lecturas.