when c++ c++11 atomic

when - c++ atomic



Entendiendo c++ 11 cercas de memoria (2)

Su uso no garantiza realmente las cosas que menciona en sus comentarios. Es decir, su uso de cercas no garantiza que sus asignaciones a sean visibles para otros subprocesos o que el valor que lea de a esté "actualizado". Esto se debe a que, aunque parezca que tiene la idea básica de dónde deben usarse las cercas, su código no cumple los requisitos exactos para que esas cercas se "sincronicen".

Aquí hay un ejemplo diferente que creo que demuestra mejor el uso correcto.

#include <iostream> #include <atomic> #include <thread> std::atomic<bool> flag(false); int a; void func1() { a = 100; atomic_thread_fence(std::memory_order_release); flag.store(true, std::memory_order_relaxed); } void func2() { while(!flag.load(std::memory_order_relaxed)) ; atomic_thread_fence(std::memory_order_acquire); std::cout << a << ''/n''; // guaranteed to print 100 } int main() { std::thread t1 (func1); std::thread t2 (func2); t1.join(); t2.join(); }

La carga y el almacenamiento en la bandera atómica no se sincronizan, ya que ambos utilizan el orden relajado de memoria. Sin las vallas, este código sería una carrera de datos, porque estamos realizando operaciones en conflicto, un objeto no atómico en diferentes hilos, y sin las cercas y la sincronización que proporcionan, no habría una relación entre las operaciones en conflicto en a .

Sin embargo, con las cercas conseguimos sincronización porque garantizamos que el subproceso 2 leerá la bandera escrita por el subproceso 1 (porque hacemos un bucle hasta que veamos ese valor), y dado que la escritura atómica ocurrió después de que se produjo la valla de liberación y la lectura atómica -Antes de adquirir la valla, las vallas se sincronizan. (Ver § 29.8 / 2 para los requisitos específicos.)

Esta sincronización significa cualquier cosa que ocurra, antes de que ocurra la barrera de liberación, antes de que ocurra algo, después de la cerca de adquisición. Por lo tanto, la escritura no atómica en a sucede antes de la lectura no atómica de a .

Las cosas se vuelven más complicadas cuando se escribe una variable en un bucle, ya que podría establecer una relación de suceso anterior para alguna iteración en particular, pero no para otras, lo que provoca una carrera de datos.

std::atomic<int> f(0); int a; void func1() { for (int i = 0; i<1000000; ++i) { a = i; atomic_thread_fence(std::memory_order_release); f.store(i, std::memory_order_relaxed); } } void func2() { int prev_value = 0; while (prev_value < 1000000) { while (true) { int new_val = f.load(std::memory_order_relaxed); if (prev_val < new_val) { prev_val = new_val; break; } } atomic_thread_fence(std::memory_order_acquire); std::cout << a << ''/n''; } }

Este código todavía hace que las vallas se sincronicen pero no elimina las carreras de datos. Por ejemplo, si f.load() devuelve 10, entonces sabemos que a=1 , a=2 , ... a=10 han sucedido todos antes de ese cout<<a particular cout<<a , pero no sabemos que cout<<a sucede-antes de a=11 . Esas son operaciones en conflicto en diferentes hilos sin una relación de suceso antes; una carrera de datos.

Estoy tratando de entender las vallas de memoria en c ++ 11, sé que hay mejores formas de hacer esto, variables atómicas, etc., pero me pregunté si este uso era correcto. Me doy cuenta de que este programa no hace nada útil, solo quería asegurarme de que el uso de las funciones de valla hiciera lo que pensé que hicieron.

Básicamente, ¿el lanzamiento asegura que cualquier cambio realizado en este hilo antes de la cerca sea visible para otros hilos después de la cerca, y que en el segundo hilo, cualquier cambio en las variables sea visible en el hilo inmediatamente después de la cerca?

¿Mi entendimiento es correcto? ¿O me he perdido el punto por completo?

#include <iostream> #include <atomic> #include <thread> int a; void func1() { for(int i = 0; i < 1000000; ++i) { a = i; // Ensure that changes to a to this point are visible to other threads atomic_thread_fence(std::memory_order_release); } } void func2() { for(int i = 0; i < 1000000; ++i) { // Ensure that this thread''s view of a is up to date atomic_thread_fence(std::memory_order_acquire); std::cout << a; } } int main() { std::thread t1 (func1); std::thread t2 (func2); t1.join(); t2.join(); }


Su uso es correcto, pero insuficiente para garantizar algo útil.

Por ejemplo, el compilador es libre de implementar internamente a = i; así si quiere:

while(a != i) { ++a; atomic_thread_fence(std::memory_order_release); }

Así que el otro hilo puede ver cualquier valor en absoluto.

Por supuesto, el compilador nunca implementaría una tarea simple como esa. Sin embargo, hay casos en los que un comportamiento igualmente desconcertante es en realidad una optimización, por lo que es una muy mala idea confiar en que el código ordinario se implemente internamente de cualquier manera particular. Es por eso que tenemos cosas como las operaciones atómicas y las cercas solo producen resultados garantizados cuando se utilizan con tales operaciones.