¿Es segura la implementación de Meyers del hilo del patrón Singleton?

145

¿Es Singletonsegura la siguiente implementación, usando la inicialización diferida, del hilo (Singleton de Meyers)?

static Singleton& instance()
{
     static Singleton s;
     return s;
}

Si no, ¿por qué y cómo hacer que sea seguro para subprocesos?

Ankur
fuente
¿Alguien puede explicar por qué esto no es seguro para subprocesos? Los artículos mencionados en los enlaces discuten la seguridad de subprocesos utilizando una implementación alternativa (utilizando una variable de puntero, es decir, Singleton * pInstance estático).
Ankur el

Respuestas:

168

En C ++ 11 , es seguro para subprocesos. De acuerdo con la norma , §6.7 [stmt.dcl] p4:

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

El soporte de GCC y VS para la función ( Inicialización dinámica y destrucción con concurrencia , también conocido como Magic Statics en MSDN ) es el siguiente:

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 de subprocesos del patrón, y la conclusión es, más o menos, que (en C ++ 03) bloqueo completo alrededor del método de creación de instancias Básicamente es la forma más sencilla de garantizar la concurrencia adecuada en todas las plataformas, mientras que la mayoría de las formas de variantes de patrón de bloqueo doblemente verificadas pueden sufrir condiciones de carrera en ciertas arquitecturas , a menos que las instrucciones se intercalen con barreras de memoria estratégicamente colocadas.

Groo
fuente
3
También hay una extensa discusión sobre el Patrón Singleton (duración y seguridad de hilos) por Alexandrescu en Modern C ++ Design. Ver el sitio de Loki: loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Matthieu M.
1
Puede crear un singleton seguro para subprocesos con boost :: call_once.
CashCow
1
Desafortunadamente, esta parte del estándar no está implementada en el compilador de Visual Studio 2012 C ++. Conocido como "Magic Statics" en la tabla "C ++ 11 Core Language Features: Concurrency" aquí: msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx
olen_garn
El fragmento del estándar aborda la construcción pero no la destrucción. ¿El estándar evita que el objeto se destruya en un subproceso mientras (o antes) otro subproceso intenta acceder al final del programa?
stewbasic
IANA (lenguaje C ++) L, pero la sección 3.6.3 [basic.start.term] p2 sugiere que es posible alcanzar un comportamiento indefinido al intentar acceder al objeto después de que ha sido destruido.
stewbasic
21

Para responder a su pregunta sobre por qué no es seguro para subprocesos, no es porque la primera llamada a instance()debe llamar al constructor Singleton s. Para ser seguro para subprocesos, esto debería ocurrir en una sección crítica, pero no hay ningún requisito en el estándar de que se tome una sección crítica (el estándar hasta la fecha es completamente silencioso en los subprocesos). Los compiladores a menudo implementan esto usando una simple verificación e incremento de un booleano estático, pero no en una sección crítica. Algo así 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 seguro para subprocesos simple (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 CRITICAL_SECTIONantes de que main()se llame. Idealmente, se usaría una verdadera clase de sección crítica RAII que pueda manejar las excepciones que podrían ocurrir cuando se realiza la sección crítica, pero eso está más allá del alcance de esta respuesta.

La operación fundamental es que cuando Singletonse solicita una instancia de , se toma un bloqueo, se crea 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 basura para "hacer un mundo mejor".

Los principales inconvenientes de esta implementación (si no dejé pasar algunos errores) es:

  • si se new Singleton()lanza, el bloqueo no se liberará. Esto se puede solucionar mediante el uso de un verdadero objeto de bloqueo RAII en lugar del simple que tengo aquí. Esto también puede ayudar a hacer 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 que main()se llame; si la llama antes (como en la inicialización de un objeto estático), las cosas podrían no funcionar porque CRITICAL_SECTIONpodrían no inicializarse.
  • Se debe tomar un candado cada vez que se solicita una instancia. Como dije, esta es una implementación simple y segura para subprocesos. Si necesita una mejor (o quiere saber por qué fallan cosas como la técnica de bloqueo de doble verificación), consulte los documentos vinculados en la respuesta de Groo .
Michael Burr
fuente
1
UH oh. ¿Qué pasa si new Singleton()tira?
sbi el
@Bob: para ser justos, con un conjunto adecuado de bibliotecas, todo lo que tiene que ver con la no copiabilidad y un bloqueo RAII adecuado desaparecería o sería mínimo. Pero quería que el ejemplo fuera razonablemente autónomo. A pesar de que los singleton son mucho trabajo para tal vez una ganancia mínima, los he encontrado útiles para administrar el uso de los globales. Tienden a hacer que sea más fácil averiguar dónde y cuándo se usan un poco mejor que una simple convención de nomenclatura.
Michael Burr el
@sbi: en este ejemplo, si se new Singleton()lanza definitivamente hay un problema con la cerradura. Se debe usar una clase de bloqueo RAII adecuada, algo así como lock_guardde Boost. Quería que el ejemplo fuera más o menos autónomo, y ya era un monstruo, así que dejé la seguridad de excepción (pero lo llamé). Tal vez debería arreglar eso para que este código no se corte y pegue en algún lugar inapropiado.
Michael Burr el
¿Por qué asignar dinámicamente el singleton? ¿Por qué no hacer que 'pInstance' sea un miembro estático de 'Singleton :: instance ()'?
Martin York el
@ Martin - hecho. Tienes razón, eso lo hace un poco más simple: sería aún mejor si usara una clase de bloqueo RAII.
Michael Burr el
10

Mirando el siguiente estándar (sección 6.7.4), explica cómo la inicialización local estática es segura para subprocesos. Entonces, una vez que esa sección del estándar se implemente ampliamente, Singleton de Meyer será la implementación preferida.

Ya no estoy de acuerdo con muchas respuestas. La mayoría de los compiladores ya implementan la inicialización estática de esta manera. La única excepción notable es Microsoft Visual Studio.

código_deft
fuente
6

La respuesta correcta depende de tu compilador. Puede decidir que sea ​​seguro para subprocesos; no es "naturalmente" seguro para los hilos.

MSalters
fuente
5

¿Es seguro el siguiente [...] hilo de implementación?

En la mayoría de las plataformas, esto no es seguro para subprocesos. (Agregue el descargo de responsabilidad habitual que explica que el estándar C ++ no conoce los subprocesos, por lo que, legalmente, no dice si lo es o no).

Si no, ¿por qué [...]?

La razón por la que no lo es es que nada impide que más de un hilo ejecute simultáneamente s'constructor'.

¿Cómo hacer que el hilo sea seguro?

"C ++ y los peligros del bloqueo de doble verificación" de Scott Meyers y Andrei Alexandrescu es un tratado bastante bueno sobre el tema de los singletons seguros para subprocesos.

sbi
fuente
2

Como dijo MSalters: Depende de la implementación de C ++ que uses. Consulta la documentación. En cuanto a la otra pregunta: "Si no, ¿por qué?" - El estándar C ++ aún no menciona nada sobre hilos. Pero la próxima versión de C ++ conoce los subprocesos y declara explícitamente que la inicialización de locales estáticos es segura para los subprocesos. Si dos hilos llaman a tal función, un hilo realizará una inicialización mientras que el otro bloqueará y esperará a que termine.

sellibitze
fuente