with threats threads thread example c++ multithreading c++11 atomic

threats - threads c++ stack overflow



Doble comprobaciĆ³n de bloqueo Singleton en C++ 11 (3)

¿La siguiente aplicación de singleton data-race es gratuita?

static std::atomic<Tp *> m_instance; ... static Tp & instance() { if (!m_instance.load(std::memory_order_relaxed)) { std::lock_guard<std::mutex> lock(m_mutex); if (!m_instance.load(std::memory_order_acquire)) { Tp * i = new Tp; m_instance.store(i, std::memory_order_release); } } return * m_instance.load(std::memory_order_relaxed); }

¿Es superfluo el std::memory_model_acquire de la operación de carga? ¿Es posible relajar aún más las operaciones de carga y almacenamiento cambiando a std::memory_order_relaxed ? En ese caso, ¿es la semántica de adquisición / liberación de std::mutex suficiente para garantizar su corrección, o también se requiere un std::atomic_thread_fence(std::memory_order_release) para garantizar que las escrituras en la memoria del constructor se produzcan antes de la tienda relajada? Sin embargo, ¿es el uso de valla equivalente a tener la tienda con memory_order_release ?

EDIT : Gracias a la respuesta de John, se me ocurrió la siguiente implementación que debería estar libre de datos. A pesar de que la carga interna podría ser absolutamente no atómica, decidí dejar una carga relajada ya que no afecta el rendimiento. En comparación con tener siempre una carga externa con el orden de memoria adquirido, la maquinaria thread_local mejora el rendimiento de acceso a la instancia de aproximadamente un orden de magnitud.

static Tp & instance() { static thread_local Tp *instance; if (!instance && !(instance = m_instance.load(std::memory_order_acquire))) { std::lock_guard<std::mutex> lock(m_mutex); if (!(instance = m_instance.load(std::memory_order_relaxed))) { instance = new Tp; m_instance.store(instance, std::memory_order_release); } } return *instance; }


Creo que esta es una gran pregunta y John Calsbeek tiene la respuesta correcta.

Sin embargo, para que quede claro, un singleton perezoso se implementa mejor usando el singleton clásico de Meyers. Ha garantizado la semántica correcta en C ++ 11.

§ 6.7.4

... Si el control ingresa la declaración simultáneamente mientras la variable se está inicializando, la ejecución concurrente esperará a que se complete la inicialización. ...

El singleton de Meyer se prefiere porque el compilador puede optimizar agresivamente el código concurrente. El compilador sería más restringido si tuviera que preservar la semántica de un std::mutex . Además, el Singleton de Meyer es de 2 líneas y es prácticamente imposible equivocarse.

Aquí está un ejemplo clásico de un singleton de Meyer. Sencillo, elegante y roto en c ++ 03. Pero simple, elegante y potente en c ++ 11.

class Foo { public: static Foo& instance( void ) { static Foo s_instance; return s_instance; } };


Esa implementación no es libre de carreras. El almacén atómico del singleton, mientras usa la semántica de lanzamiento, solo se sincronizará con la operación de adquisición correspondiente, es decir, la operación de carga que ya está protegida por el mutex.

Es posible que la carga relajada exterior lea un puntero no nulo antes de que el hilo de bloqueo termine de inicializar el singleton.

La adquisición que está protegida por la cerradura, por otro lado, es redundante. Se sincronizará con cualquier tienda con semántica de lanzamiento en otro subproceso, pero en ese punto (gracias al mutex) el único subproceso que posiblemente puede almacenar es el subproceso actual. Esa carga ni siquiera necesita ser atómica, no pueden ocurrir almacenes desde otro hilo.

Vea la serie de Anthony Williams sobre multiproceso en C ++ 0x .


Véase también call_once . Donde anteriormente usaba un singleton para hacer algo, pero en realidad no usaba el objeto devuelto para nada, call_once podría ser la mejor solución. Para un singleton normal, puede hacer call_once para establecer una variable (¿global?) Y luego devolver esa variable ...

Simplificado por brevedad:

template< class Function, class... Args> void call_once( std::once_flag& flag, Function&& f, Args&& args...);

  • Se realiza exactamente una ejecución de exactamente una de las funciones, pasada como f a las invocaciones en el grupo (el mismo objeto de bandera)

  • No se devuelve ninguna invocación en el grupo antes de que la ejecución antes mencionada de la función seleccionada se complete con éxito