tiendas real postuzapas opiniones online negra lista fiable c++ multithreading atomic memory-order

c++ - real - ¿Pueden los atómicos sufrir tiendas falsas?



postuzapas es real (3)

Las respuestas existentes proporcionan una gran cantidad de buena explicación, pero no dan una respuesta directa a su pregunta. Aquí vamos:

¿Pueden los atómicos sufrir espurias?

Sí, pero no puede observarlos desde un programa C ++ que está libre de carreras de datos.

Solo el volatile tiene prohibido realizar accesos de memoria adicionales.

¿El modelo de memoria C ++ prohíbe que el hilo 1 se comporte como si lo hiciera?

++m; ++m;

Sí, pero este está permitido:

lock (shared_std_atomic_secret_lock) { ++m; ++m; }

Está permitido pero estúpido. Una posibilidad más realista es convertir esto:

std::atomic<int64_t> m; ++m;

dentro

memory_bus_lock { ++m.low; if (last_operation_did_carry) ++m.high; }

donde memory_bus_lock y last_operation_did_carry son características de la plataforma de hardware que no se pueden expresar en C ++ portátil.

Tenga en cuenta que los periféricos que se encuentran en el bus de memoria ven el valor intermedio, pero pueden interpretar esta situación correctamente observando el bloqueo del bus de memoria. Los depuradores de software no podrán ver el valor intermedio.

En otros casos, las operaciones atómicas pueden implementarse mediante bloqueos de software, en cuyo caso:

  1. Los depuradores de software pueden ver valores intermedios y deben tener en cuenta el bloqueo del software para evitar interpretaciones erróneas.
  2. Los periféricos de hardware verán cambios en el bloqueo del software y valores intermedios del objeto atómico. Es posible que se requiera algo de magia para que el periférico reconozca la relación entre los dos.
  3. Si el objeto atómico está en la memoria compartida, otros procesos pueden ver los valores intermedios y es posible que no tengan forma de inspeccionar el bloqueo del software / pueden tener una copia separada de dicho bloqueo del software
  4. Si otros hilos en el mismo programa C ++ rompen la seguridad del tipo de una manera que causa una carrera de datos (por ejemplo, usando memcpy para leer el objeto atómico) pueden observar valores intermedios. Formalmente, eso es un comportamiento indefinido.

Un último punto importante. La "escritura especulativa" es un escenario muy complejo. Es más fácil ver esto si cambiamos el nombre de la condición:

Tema # 1

if (my_mutex.is_held) o += 2; // o is an ordinary variable, not atomic or volatile return o;

Hilo # 2

{ scoped_lock l(my_mutex); return o; }

No hay carrera de datos aquí. Si el hilo # 1 tiene el mutex bloqueado, la escritura y la lectura no pueden ocurrir desordenadas. Si no tiene el mutex bloqueado, los subprocesos se ejecutan desordenados, pero ambos solo realizan lecturas.

Por lo tanto, el compilador no puede permitir que se vean valores intermedios. Este código C ++ no es una reescritura correcta:

o += 2; if (!my_mutex.is_held) o -= 2;

porque el compilador inventó una carrera de datos. Sin embargo, si la plataforma de hardware proporciona un mecanismo para escrituras especulativas sin carrera (¿quizás Itanium?), El compilador puede usarlo. Entonces el hardware puede ver valores intermedios, aunque el código C ++ no.

Si el hardware no debe ver los valores intermedios, debe usar los volatile (posiblemente además de los atómicos, ya que no se garantiza la lectura volatile lectura, escritura y modificación atómicas). Con la volatile , solicitar una operación que no se puede realizar tal como está escrita dará como resultado una falla de compilación, no un acceso de memoria espurio.

En C ++, ¿pueden los átomos sufrir tiendas falsas?

Por ejemplo, supongamos que myn son atómicos y que m = 5 inicialmente. En el hilo 1,

m += 2;

En el hilo 2,

n = m;

Resultado: el valor final de n debe ser 5 o 7, ¿verdad? ¿Pero podría ser falsamente 6? ¿Podría ser falsamente 4 u 8, o incluso algo más?

En otras palabras, ¿el modelo de memoria C ++ prohíbe que el hilo 1 se comporte como si lo hiciera?

++m; ++m;

O, más raro, como si hiciera esto?

tmp = m; m = 4; tmp += 2; m = tmp;

Referencia: H.-J. Boehm & SV Adve, 2008, Figura 1. (Si sigue el enlace, en la sección 1 del documento, vea el primer elemento con viñetas: "Las especificaciones informales proporcionadas por ...")

LA PREGUNTA EN FORMA ALTERNATIVA

Una respuesta (apreciada) muestra que la pregunta anterior puede malinterpretarse. Si es útil, entonces aquí está la pregunta en forma alternativa.

Supongamos que el programador intentó indicar al hilo 1 que omitiera la operación:

bool a = false; if (a) m += 2;

¿El modelo de memoria C ++ prohíbe que el hilo 1 se comporte, en tiempo de ejecución, como si lo hiciera?

m += 2; // speculatively alter m m -= 2; // oops, should not have altered! reverse the alteration

Lo pregunto porque Boehm y Adve, vinculados anteriormente, parecen explicar que una ejecución multiproceso puede

  • especulativamente alterar una variable, pero luego
  • luego cambia la variable a su valor original cuando la alteración especulativa resulta innecesaria.

CÓDIGO DE MUESTRA COMPILABLE

Aquí hay un código que puedes compilar, si lo deseas.

#include <iostream> #include <atomic> #include <thread> // For the orignial question, do_alter = true. // For the question in alternate form, do_alter = false. constexpr bool do_alter = true; void f1(std::atomic_int *const p, const bool do_alter_) { if (do_alter_) p->fetch_add(2, std::memory_order_relaxed); } void f2(const std::atomic_int *const p, std::atomic_int *const q) { q->store( p->load(std::memory_order_relaxed), std::memory_order_relaxed ); } int main() { std::atomic_int m(5); std::atomic_int n(0); std::thread t1(f1, &m, do_alter); std::thread t2(f2, &m, &n); t2.join(); t1.join(); std::cout << n << "/n"; return 0; }

Este código siempre imprime 5 o 7 cuando lo ejecuto. (De hecho, hasta donde puedo decir, siempre imprime 7 cuando lo ejecuto). Sin embargo, no veo nada en la semántica que impida que imprima 6 , 4 u 8 .

El excelente Cppreference.com states, "los objetos atómicos están libres de carreras de datos", lo cual es bueno, pero en un contexto como este, ¿qué significa?

Sin dudas, todo esto significa que no entiendo muy bien la semántica. Cualquier iluminación que pueda arrojar sobre la pregunta sería apreciada.

RESPUESTAS

@Christophe, @ZalmanStern y @BenVoigt iluminan la pregunta con habilidad. Sus respuestas cooperan en lugar de competir. En mi opinión, los lectores deben prestar atención a las tres respuestas: @Christophe primero; @ZalmanStern segundo; y @BenVoigt último para resumir.


Su código hace uso de fetch_add() en el atómico, lo que le da la siguiente garantía:

Reemplaza atómicamente el valor actual con el resultado de la suma aritmética del valor y arg. La operación es una operación de lectura-modificación-escritura. La memoria se ve afectada según el valor del pedido.

La semántica es clara como el cristal: antes de la operación es m, después de la operación es m + 2, y ningún hilo accede a lo que hay entre estos dos estados porque la operación es atómica.

Editar: elementos adicionales con respecto a su pregunta alternativa

Independientemente de lo que digan Boehm y Adve, los compiladores de C ++ obedecen a la siguiente cláusula estándar:

1.9 / 5: Una implementación conforme que ejecuta un programa bien formado producirá el mismo comportamiento observable que una de las posibles ejecuciones de la instancia correspondiente de la máquina abstracta con el mismo programa y la misma entrada.

Si un compilador de C ++ generara código que pudiera permitir que las actualizaciones especulativas interfieran con el comportamiento observable del programa (también conocido como obtener algo más que 5 o 7), no cumpliría con el estándar, ya que no garantizaría la garantía mencionada en mi respuesta inicial


Su pregunta revisada difiere bastante de la primera en que hemos pasado de consistencia secuencial a orden de memoria relajado.

Tanto razonar sobre y especificar ordenamientos débiles de memoria puede ser bastante complicado. Por ejemplo, observe la diferencia entre la especificación de C ++ 11 y C ++ 14 señalada aquí: http://en.cppreference.com/w/cpp/atomic/memory_order#Relaxed_ordering . Sin embargo, la definición de atomicidad impide que la llamada fetch_add permita que cualquier otro subproceso vea valores distintos de los que se escriben en la variable o uno de ellos más 2. (Un subproceso puede hacer casi cualquier cosa siempre que garantice los valores intermedios no son observables por otros hilos).

(Para ser terriblemente específico, es probable que desee buscar "leer-modificar-escribir" en la especificación de C ++, por ejemplo, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf )

Quizás ayudar con una referencia específica al lugar en el documento vinculado sobre el que tenga preguntas. Ese documento es anterior a la primera especificación del modelo de memoria concurrente de C ++ (en C ++ 11) por un poquito y ahora tenemos otra revolución más allá de eso, por lo que también puede estar un poco desactualizado con respecto a lo que dice el estándar, aunque Espero que esto sea más una cuestión de proponer cosas que podrían suceder en variables no atómicas.

EDITAR: Agregaré un poco más sobre "la semántica" para quizás ayudar a pensar en cómo analizar este tipo de cosas.

El objetivo de la ordenación de la memoria es establecer un conjunto de posibles órdenes entre lecturas y escrituras a variables entre subprocesos. En ordenamientos más débiles, no se garantiza que haya un solo pedido global que se aplique a todos los hilos. Esto solo ya es lo suficientemente complicado como para asegurarse de que se entienda completamente antes de continuar.

Dos cosas involucradas en la especificación de un pedido son direcciones y operaciones de sincronización. En efecto, una operación de sincronización tiene dos lados y esos dos lados están conectados mediante el intercambio de una dirección. (Se puede considerar que una valla se aplica a todas las direcciones). Mucha de la confusión en el espacio proviene de averiguar cuándo una operación de sincronización en una dirección garantiza algo para otras direcciones. Por ejemplo, las operaciones de bloqueo y desbloqueo de mutex solo establecen el orden a través de las operaciones de adquisición y liberación en las direcciones dentro del mutex, pero esa sincronización se aplica a todas las lecturas y escrituras de los subprocesos que bloquean y desbloquean el mutex. Una variable atómica a la que se accede utilizando un orden relajado impone pocas restricciones sobre lo que sucede, pero esos accesos pueden tener restricciones de ordenamiento impuestas por operaciones más fuertemente ordenadas en otras variables atómicas o mutexes.

Las principales operaciones de sincronización son acquire y release . Ver: http://en.cppreference.com/w/cpp/atomic/memory_order . Estos son nombres por lo que pasa con un mutex. La operación de adquisición se aplica a las cargas y evita que las operaciones de memoria en el subproceso actual se reordenen más allá del punto donde ocurre la adquisición. También establece un pedido con cualquier operación de liberación previa en la misma variable. El último bit está gobernado por el valor cargado. Es decir, si la carga devuelve un valor de una escritura dada con sincronización de liberación, la carga ahora se ordena contra esa escritura y todas las otras operaciones de memoria por esos hilos caen en su lugar de acuerdo con las reglas de ordenamiento.

Las operaciones atómicas o de lectura-modificación-escritura son su propia pequeña secuencia en el orden más grande. Se garantiza que la lectura, la operación y la escritura se realicen de forma atómica. Cualquier otro orden viene dado por el parámetro de orden de memoria a la operación. Por ejemplo, la especificación de un orden relajado indica que, de lo contrario, no se aplicarán restricciones a ninguna otra variable. Es decir, no hay ninguna adquisición o liberación implicada por la operación. Especificar memory_order_acq_rel dice que no solo la operación es atómica, sino que la lectura es una adquisición y la escritura es una versión; si el hilo lee un valor de otra escritura con semántica de lanzamiento, todos los demás átomos ahora tienen la restricción de orden apropiada en este hilo.

Se puede usar un orden fetch_add con memoria relajada para un contador de estadísticas en la creación de perfiles. Al final de la operación, todos los subprocesos habrán hecho algo más para asegurar que todos los incrementos de contador ahora sean visibles para el lector final, pero en el estado intermedio no nos importa mientras el total final se sume. Sin embargo, esto no implica que las lecturas intermedias puedan muestrear valores que nunca fueron parte del conteo. Por ejemplo, si siempre estamos agregando valores pares a un contador que comienza en 0, ningún hilo debería leer un valor impar, independientemente de su orden.

Estoy un poco desconcertado por no ser capaz de señalar un texto específico en el estándar que dice que no puede haber efectos secundarios a las variables atómicas distintas de las codificadas explícitamente en el programa de alguna manera. Muchas cosas mencionan los efectos secundarios, pero parece darse por sentado que los efectos secundarios son los especificados por la fuente y no los inventados por el compilador. No tengo tiempo para rastrear esto ahora mismo, pero hay muchas cosas que no funcionarían si esto no estuviera garantizado y parte del punto de std::atomic es obtener esta restricción ya que no está garantizada por otra variables. (Es algo provisto por volatile , o al menos está destinado a ser. Parte de la razón por la que tenemos este grado de especificación para ordenar la memoria en std::atomic es porque volatile nunca llegó a ser lo suficientemente bien especificado para razonar en detalle y nadie conjunto de restricciones cubrió todas las necesidades.)