¿Es segura la implementación de Meyers del hilo del patrón Singleton?
¿Es segura la siguiente implementación, que utiliza inicialización diferida, del Singleton
subproceso (Singleton de Meyers)?
static Singleton& instance()
{
static Singleton s;
return s;
}
Si no es así, ¿por qué y cómo hacerlo seguro para subprocesos?
En C++11 , es seguro para subprocesos. Según la norma :§6.7 [stmt.dcl] p4
Si el control ingresa la declaración simultáneamente mientras se inicializa la variable, la ejecución simultánea esperará hasta que se complete la inicialización.
La compatibilidad con GCC y VS para la función ( Inicialización y destrucción dinámica con simultaneidad , también conocida como Magic Statics en MSDN ) es la siguiente:
- Visual Studio: compatible desde Visual Studio 2015
- GCC: compatible desde GCC 4.3
Gracias a @Mankarse y @olen_gam por sus comentarios.
En C++03 , este código no era seguro para subprocesos. Hay un artículo de Meyers llamado "C++ y los peligros del bloqueo de doble verificación" que analiza las implementaciones seguras para subprocesos del patrón, y la conclusión es, más o menos, que (en C++03) el bloqueo completo en torno al método de creación de instancias es básicamente la forma más sencilla de garantizar una concurrencia adecuada en todas las plataformas, mientras que la mayoría de las formas de variantes de patrones de bloqueo de doble verificación pueden sufrir condiciones de carrera en ciertas arquitecturas , a menos que las instrucciones estén intercaladas con barreras de memoria ubicadas estratégicamente.
Para responder a su pregunta sobre por qué no es seguro para subprocesos, no es porque la primera llamada deba instance()
llamar al constructor for Singleton s
. Para ser seguro para subprocesos, esto tendría que ocurrir en una sección crítica, pero no existe ningún requisito en el estándar de que se tome una sección crítica (el estándar hasta la fecha no dice nada sobre los subprocesos). Los compiladores a menudo implementan esto mediante una simple verificación e incremento de un booleano estático, pero no en una sección crítica. Algo como el siguiente pseudocódigo:
static Singleton& instance()
{
static bool initialized = false;
static char s[sizeof( Singleton)];
if (!initialized) {
initialized = true;
new( &s) Singleton(); // call placement new on s to construct it
}
return (*(reinterpret_cast<Singleton*>( &s)));
}
Así que aquí hay un Singleton simple y seguro para subprocesos (para Windows). Utiliza un contenedor de clase simple para el objeto CRITICAL_SECTION de Windows para que podamos hacer que el compilador inicialice automáticamente la llamada CRITICAL_SECTION
anterior . main()
Lo ideal sería utilizar una verdadera clase de sección crítica RAII que pueda manejar las excepciones que podrían ocurrir cuando se mantiene la sección crítica, pero eso está más allá del alcance de esta respuesta.
La operación fundamental es que cuando Singleton
se solicita una instancia de, se toma un bloqueo, se crea el Singleton si es necesario, luego se libera el bloqueo y se devuelve la referencia Singleton.
#include <windows.h>
class CritSection : public CRITICAL_SECTION
{
public:
CritSection() {
InitializeCriticalSection( this);
}
~CritSection() {
DeleteCriticalSection( this);
}
private:
// disable copy and assignment of CritSection
CritSection( CritSection const&);
CritSection& operator=( CritSection const&);
};
class Singleton
{
public:
static Singleton& instance();
private:
// don't allow public construct/destruct
Singleton();
~Singleton();
// disable copy & assignment
Singleton( Singleton const&);
Singleton& operator=( Singleton const&);
static CritSection instance_lock;
};
CritSection Singleton::instance_lock; // definition for Singleton's lock
// it's initialized before main() is called
Singleton::Singleton()
{
}
Singleton& Singleton::instance()
{
// check to see if we need to create the Singleton
EnterCriticalSection( &instance_lock);
static Singleton s;
LeaveCriticalSection( &instance_lock);
return s;
}
Hombre, eso es un montón de tonterías para "hacer un mundo mejor".
Los principales inconvenientes de esta implementación (si no dejé escapar algunos errores) son:
- Si
new Singleton()
se lanza, el bloqueo no se desbloqueará. Esto se puede solucionar usando un objeto de bloqueo RAII verdadero en lugar del simple que tengo aquí. Esto también puede ayudar a que las cosas sean portátiles si usa algo como Boost para proporcionar un contenedor independiente de la plataforma para la cerradura. - esto garantiza la seguridad de los subprocesos cuando se solicita la instancia Singleton después de
main()
llamarla; si la llama antes (como en la inicialización de un objeto estático), es posible que las cosas no funcionen porque esCRITICAL_SECTION
posible que no se inicialice. - se debe tomar un bloqueo cada vez que se solicita una instancia. Como dije, esta es una implementación simple y segura para subprocesos. Si necesita uno mejor (o quiere saber por qué cosas como la técnica de bloqueo de doble verificación es defectuosa), consulte los artículos vinculados en la respuesta de Groo .