Doble comprobación de Lock Singleton en C++11


¿Los siguientes datos de implementación de singleton están libres de carreras?

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 std::memory_model_acquire de la operación de carga superflua? ¿Es posible relajar aún más las operaciones de carga y almacenamiento cambiándolas a std::memory_order_relaxed? En ese caso, ¿es suficiente la semántica de acquire/release de std::mutex para garantizar su corrección, o también se requiere otro std::atomic_thread_fence(std::memory_order_release) para asegurar que las escrituras en la memoria del constructor sucedan 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 ser libre de carreras de datos. A pesar de que la carga interna podría ser no atómica en absoluto, decidí dejar una carga relajada en que no afecta el rendimiento. En comparación con tener siempre una carga externa con el orden de adquisición de memoria, la maquinaria thread_local mejora el rendimiento de acceder 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;
}
Author: Xeo, 2011-05-22

3 answers

Esa implementación es no libre de razas. El almacén atómico del singleton, mientras utiliza la semántica de liberación, 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 externa 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 liberación en otro hilo, pero en ese punto (gracias al mutex) el único hilo que puede almacenar es el hilo actual. Esa carga ni siquiera tiene que ser atómica - no se puede almacenar de otro hilo.

Ver La serie de Anthony Williams sobre C++0x multithreading.

 20
Author: John Calsbeek,
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-05-22 12:43:11

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

Sin embargo, solo para ser claros, un singleton perezoso se implementa mejor utilizando el clásico Meyers singleton. Ha garantizado la semántica correcta en C++11.

§ 6.7.4

... Si el control entra la declaración simultáneamente mientras la variable se inicializa, la ejecución concurrente esperará finalización de la inicialización. ...

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

Aquí está un ejemplo clásico de un singleton de Meyer. Simple, 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;
   }
};
 27
Author: deft_code,
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-05-25 21:27:10

Véase también call_once. Donde previamente usarías un singleton para hacer algo, pero en realidad no usarías el objeto devuelto para nada, call_once puede ser la mejor solución. Para un singleton regular que podría hacer call_once para establecer un (global?) variable y luego devuelve esa variable...

Simplificado para la brevedad:

template< class Function, class... Args>
void call_once( std::once_flag& flag, Function&& f, Args&& args...);
  • Exactly one execution of exactly one of the functions, passed as f to the invocations in the group (same flag object), se realiza.

  • 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

 7
Author: MaHuJa,
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-11-20 16:31:29