singleton eficiente y seguro para subprocesos en C++


El patrón habitual para una clase singleton es algo así como

static Foo &getInst()
{
  static Foo *inst = NULL;
  if(inst == NULL)
    inst = new Foo(...);
  return *inst;    
}

Sin embargo, entiendo que esta solución no es segura para subprocesos, ya que 1) El constructor de Foo puede ser llamado más de una vez (lo que puede o no importar) y 2) inst puede no estar completamente construido antes de que sea devuelto a un subproceso diferente.

Una solución es envolver un mutex alrededor de todo el método, pero luego estoy pagando por la sobrecarga de sincronización mucho después de que realmente lo necesite. Una alternativa es algo así como

static Foo &getInst()
{
  static Foo *inst = NULL;
  if(inst == NULL)
  {
    pthread_mutex_lock(&mutex);
    if(inst == NULL)
      inst = new Foo(...);
    pthread_mutex_unlock(&mutex);
  }
  return *inst;    
}

¿Es esta la manera correcta de hacerlo, o hay alguna trampa de la que debería estar consciente? Por ejemplo, ¿hay algún problema de orden de inicialización estática que pueda ocurrir, es decir, siempre se garantiza que inst sea NULL la primera vez que se llama a getInst?

Author: user168715, 2010-04-05

9 answers

Su solución se llama 'bloqueo de doble comprobación' y la forma en que la ha escrito no es segura para hilos.

Este documento de Meyers/Alexandrescu explica por qué - pero ese documento también es ampliamente malentendido. Comenzó el meme ' el bloqueo de doble comprobación no es seguro en C++', pero su conclusión real es que el bloqueo de doble comprobación en C++ se puede implementar de forma segura, solo requiere el uso de barreras de memoria en un lugar no obvio.

El documento contiene pseudocódigo que demuestra cómo usar barreras de memoria para implementar de forma segura el DLCP, por lo que no debería ser difícil para usted corregir su implementación.

 39
Author: Joe Gauterin,
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
2010-04-05 11:54:45

Si está utilizando C++11, aquí hay una manera correcta de hacer esto:

Foo& getInst()
{
    static Foo inst(...);
    return inst;
}

De acuerdo con el nuevo estándar, ya no hay necesidad de preocuparse por este problema. La inicialización del objeto se hará solo por un hilo, otros hilos esperarán hasta que se complete. O puedes usar std:: call_once. (más información aquí)

 73
Author: Sat,
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
2013-11-12 15:29:35

Herb Sutter habla sobre el doble control de bloqueo en CppCon 2014.

A continuación se muestra el código que implementé en C++11 basado en eso:

class Foo {
public:
    static Foo* Instance();
private:
    Foo() {}
    static atomic<Foo*> pinstance;
    static mutex m_;
};

atomic<Foo*> Foo::pinstance { nullptr };
std::mutex Foo::m_;

Foo* Foo::Instance() {
  if(pinstance == nullptr) {
    lock_guard<mutex> lock(m_);
    if(pinstance == nullptr) {
        pinstance = new Foo();
    }
  }
  return pinstance;
}

También puede consultar el programa completo aquí: http://ideone.com/olvK13

 10
Author: qqibrow,
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
2015-01-16 04:35:28

Uso pthread_once, que se garantiza que la función de inicialización se ejecuta una vez atómicamente.

(En Mac OS X utiliza un bloqueo de giro. No conozco la implementación de otras plataformas.)

 8
Author: kennytm,
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
2010-04-04 21:57:24

TTBOMK, la única forma segura de hacer esto sin bloqueo sería inicializar todos sus singletons antes de de iniciar un hilo.

 2
Author: sbi,
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
2010-04-04 22:18:35

Su alternativa se llama "bloqueo doble comprobado".

Podrían existir modelos de memoria multihilo en los que funcione, pero POSIX no garantiza uno

 0
Author: Steve Jessop,
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
2010-04-04 22:52:06

La implementación de ACE singleton utiliza un patrón de bloqueo doble comprobado para la seguridad del hilo, puede referirse a él si lo desea.

Puede encontrar el código fuente aquí.

 0
Author: baris.aydinoz,
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
2010-04-05 06:51:40

¿Funciona TLS aquí? https://en.wikipedia.org/wiki/Thread-local_storage#C_and_C++

Por ejemplo,

static _thread Foo *inst = NULL;
static Foo &getInst()
{
  if(inst == NULL)
    inst = new Foo(...);
  return *inst;    
 }

Pero también necesitamos una forma de eliminarlo explícitamente, como

static void deleteInst() {
   if (!inst) {
     return;
   }
   delete inst;
   inst = NULL;
}
 0
Author: Joe C,
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
2018-04-15 16:58:37

La solución no es segura para el hilo porque la instrucción

inst = new Foo();

Se puede dividir en dos declaraciones por compilador:

inst = malloc(sizeof(Foo));   
inst->Foo();

Así que si después de la ejecución de la instrucción 1 por un hilo, otro hilo ejecuta el método getInstance(), entonces encontrará que el puntero no es null y luego devolverá el puntero al objeto no iniciado.

 -1
Author: Ishek ҆,
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
2018-07-07 16:56:36