c++ singleton thread-safety pthreads

singleton eficiente para subprocesos en C++



thread-safety pthreads (9)

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

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

Sin embargo, tengo entendido que esta solución no es segura para subprocesos, ya que 1) el constructor de Foo puede llamarse más de una vez (lo que puede o no puede importar) y 2) inst puede no estar completamente construido antes de que se devuelva a un subproceso diferente .

Una solución es envolver un mutex alrededor de todo el método, pero luego estoy pagando la sobrecarga de sincronización mucho después de que realmente la necesito. Una alternativa es algo 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 forma correcta de hacerlo, o hay alguna trampa que debería tener en cuenta? Por ejemplo, ¿existen problemas de orden de inicialización estática que puedan ocurrir, es decir, siempre se garantiza que inst es NULL la primera vez que se llama a getInst?


¿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 manera de eliminarlo explícitamente, como

static void deleteInst() { if (!inst) { return; } delete inst; inst = NULL; }


La implementación de ACE singleton utiliza un patrón de bloqueo de doble comprobación para la seguridad de subprocesos, puede consultarla si lo desea.

Puedes encontrar el código fuente here .


La solución no es segura para subprocesos porque la declaración

inst = new Foo();

se puede dividir en dos declaraciones por compilador:

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

Entonces, si después de la ejecución de la instrucción 1 por un hilo, otro hilo ejecuta el método getInstance() , encontrará que el puntero no es nulo y luego devolverá el puntero al objeto sin inicializar.


Si está utilizando C ++ 11, esta es una forma correcta de hacer esto:

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

Según la nueva norma, 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 here )



Su solución se llama "doble control de bloqueo" 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 muy mal entendido. Comenzó el ''bloqueo de doble comprobación no es seguro en el meme de 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 un pseudocódigo que muestra cómo usar las barreras de memoria para implementar de manera segura el DLCP, por lo que no debería ser difícil para usted corregir su implementación.


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


Use pthread_once , que garantiza que la función de inicialización se ejecute una vez de forma atómica.

(En Mac OS X usa un bloqueo de giro. No se conoce la implementación de otras plataformas).


Herb Sutter habla sobre el bloqueo de doble control 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