Pregunta

¿Hay alguna manera de implementar un objeto singleton en C++ que sea:

  1. Construido de manera perezosa de manera segura para subprocesos (dos subprocesos pueden ser simultáneamente el primer usuario del singleton; de todos modos, solo debe construirse una vez).
  2. No depende de que las variables estáticas se construyan de antemano (por lo que el objeto singleton es seguro de usar durante la construcción de variables estáticas).

(No conozco lo suficiente mi C++, pero ¿se da el caso de que las variables estáticas integrales y constantes se inicialicen antes de ejecutar cualquier código (es decir, incluso antes de que se ejecuten los constructores estáticos? Es posible que sus valores ya estén "inicializados" en el programa). imagen)?Si es así, tal vez esto pueda aprovecharse para implementar un mutex singleton, que a su vez puede usarse para proteger la creación del singleton real...)


Excelente, parece que ahora tengo un par de buenas respuestas (lástima que no puedo marcar 2 o 3 como la respuesta).Parece haber dos soluciones generales:

  1. Utilice la inicialización estática (a diferencia de la inicialización dinámica) de una variable estática POD e implemente mi propio mutex con eso utilizando las instrucciones atómicas integradas.Este era el tipo de solución que insinuaba en mi pregunta y creo que ya lo sabía.
  2. Utilice alguna otra función de biblioteca como pthread_once o impulso::call_once.Ciertamente no los conocía y estoy muy agradecido por las respuestas publicadas.
¿Fue útil?

Solución

Básicamente, estás solicitando la creación sincronizada de un singleton, sin utilizar ninguna sincronización (variables previamente construidas).En general no, esto no es posible.Necesita algo disponible para la sincronización.

En cuanto a su otra pregunta, sí, las variables estáticas que se pueden inicializar estáticamente (es decir,no se necesita código de tiempo de ejecución) se garantiza que se inicializarán antes de que se ejecute otro código.Esto hace posible utilizar un mutex inicializado estáticamente para sincronizar la creación del singleton.

De la revisión de 2003 del estándar C++:

Los objetos con duración de almacenamiento estático (3.7.1) se inicializarán en cero (8.5) antes de que tenga lugar cualquier otra inicialización.La inicialización cero y la inicialización con una expresión constante se denominan colectivamente inicialización estática;todas las demás inicializaciones son inicializaciones dinámicas.Los objetos de tipos POD (3.9) con duración de almacenamiento estático inicializados con expresiones constantes (5.19) se inicializarán antes de que tenga lugar cualquier inicialización dinámica.Los objetos con duración de almacenamiento estático definidos en el alcance del espacio de nombres en la misma unidad de traducción e inicializados dinámicamente se inicializarán en el orden en que aparece su definición en la unidad de traducción.

Si usted saber Si utiliza este singleton durante la inicialización de otros objetos estáticos, creo que encontrará que la sincronización no es un problema.Hasta donde yo sé, todos los compiladores principales inicializan objetos estáticos en un solo subproceso, por lo que la seguridad de los subprocesos durante la inicialización estática.Puede declarar su puntero singleton como NULL y luego verificar si se ha inicializado antes de usarlo.

Sin embargo, esto supone que usted saber que usará este singleton durante la inicialización estática.Esto tampoco está garantizado por el estándar, por lo que si desea estar completamente seguro, utilice un mutex inicializado estáticamente.

Editar:La sugerencia de Chris de utilizar una comparación e intercambio atómico ciertamente funcionaría.Si la portabilidad no es un problema (y la creación de singletons temporales adicionales no es un problema), entonces es una solución indirecta ligeramente menor.

Otros consejos

Desafortunadamente, la respuesta de Matt presenta lo que se llama bloqueo de doble control que no es compatible con el modelo de memoria C/C++.(Es compatible con el modelo de memoria Java 1.5 y posterior, y creo que .NET). Esto significa que entre el momento en que pObj == NULL se realiza la verificación y cuando se adquiere el bloqueo (mutex), pObj Es posible que ya haya sido asignado en otro hilo.El cambio de subproceso ocurre cuando el sistema operativo lo desea, no entre "líneas" de un programa (que no tienen significado posterior a la compilación en la mayoría de los idiomas).

Además, como Matt reconoce, utiliza un int como un bloqueo en lugar de una primitiva del sistema operativo.No hagas eso.Los bloqueos adecuados requieren el uso de instrucciones de barrera de memoria, potencialmente vaciados de líneas de caché, etc.;utilice las primitivas de su sistema operativo para bloquear.Esto es especialmente importante porque las primitivas utilizadas pueden cambiar entre las líneas de CPU individuales en las que se ejecuta su sistema operativo;Lo que funciona en una CPU Foo puede no funcionar en una CPU Foo2.La mayoría de los sistemas operativos admiten de forma nativa subprocesos POSIX (pthreads) o los ofrecen como contenedor para el paquete de subprocesos del sistema operativo, por lo que suele ser mejor ilustrar ejemplos usándolos.

Si su sistema operativo ofrece primitivas apropiadas, y si las necesita absolutamente para el rendimiento, en lugar de realizar este tipo de bloqueo/inicialización, puede usar un comparación e intercambio atómico operación para inicializar una variable global compartida.Básicamente, lo que escribas se verá así:

MySingleton *MySingleton::GetSingleton() {
    if (pObj == NULL) {
        // create a temporary instance of the singleton
        MySingleton *temp = new MySingleton();
        if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) {
            // if the swap didn't take place, delete the temporary instance
            delete temp;
        }
    }

    return pObj;
}

Esto solo funciona si es seguro crear varias instancias de su singleton (una por subproceso que invoca GetSingleton() simultáneamente) y luego desechar las extras.El OSAtomicCompareAndSwapPtrBarrier función proporcionada en Mac OS X (la mayoría de los sistemas operativos proporcionan una primitiva similar) verifica si pObj es NULL y en realidad sólo lo establece en temp a ello si lo es.Esto utiliza soporte de hardware para realmente, literalmente, solo realizar el intercambio. una vez y decir si sucedió.

Otra función que puedes aprovechar si tu sistema operativo lo ofrece y que se encuentra entre estos dos extremos es pthread_once.Esto le permite configurar una función que se ejecuta solo una vez, básicamente haciendo todo el bloqueo/barrera/etc.engaño para usted, sin importar cuántas veces se invoque o en cuántos subprocesos se invoque.

Aquí hay un captador singleton muy simple construido de manera perezosa:

Singleton *Singleton::self() {
    static Singleton instance;
    return &instance;
}

Esto es lento y el próximo estándar de C++ (C++0x) requiere que sea seguro para subprocesos.De hecho, creo que al menos g++ implementa esto de manera segura para subprocesos.Entonces, si ese es tu compilador objetivo o si usa un compilador que también implemente esto de manera segura para subprocesos (¿tal vez los compiladores más nuevos de Visual Studio lo hagan?No lo sé), entonces esto podría ser todo lo que necesitas.

Ver también http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html sobre este tema.

No puede hacerlo sin variables estáticas; sin embargo, si está dispuesto a tolerar una, puede usar Impulso.Hilo para este propósito.Lea la sección "inicialización única" para obtener más información.

Luego, en su función de acceso singleton, use boost::call_once para construir el objeto y devolverlo.

Para gcc, esto es bastante sencillo:

LazyType* GetMyLazyGlobal() {
    static const LazyType* instance = new LazyType();
    return instance;
}

GCC se asegurará de que la inicialización sea atómica. Para VC++, este no es el caso. :-(

Un problema importante con este mecanismo es la falta de capacidad de prueba:Si necesita restablecer el LazyType a uno nuevo entre pruebas, o desea cambiar el LazyType* a un MockLazyType*, no podrá hacerlo.Teniendo esto en cuenta, normalmente es mejor utilizar un mutex estático + un puntero estático.

Además, posiblemente un comentario aparte:Es mejor evitar siempre los tipos estáticos que no sean POD.(Las referencias a los POD están bien). Las razones de esto son muchas:Como usted menciona, el orden de inicialización no está definido; tampoco lo está el orden en el que se llaman los destructores.Debido a esto, los programas terminarán fallando cuando intenten salir;A menudo no es gran cosa, pero a veces es un problema cuando el generador de perfiles que estás intentando utilizar requiere una salida limpia.

Si bien esta pregunta ya ha sido respondida, creo que hay algunos otros puntos que mencionar:

  • Si desea una instanciación diferida del singleton mientras usa un puntero a una instancia asignada dinámicamente, deberá asegurarse de limpiarlo en el punto correcto.
  • Podría usar la solución de Matt, pero necesitaría usar una sección mutex/crítica adecuada para bloquear y marcar "pObj == NULL" antes y después del bloqueo.Por supuesto, pObj también tendría que ser estático ;).Un mutex sería innecesariamente pesado en este caso, sería mejor optar por una sección crítica.

Pero como ya se indicó, no se puede garantizar una inicialización diferida segura para subprocesos sin utilizar al menos una primitiva de sincronización.

Editar:Sí Derek, tienes razón.Culpa mía.:)

Podría usar la solución de Matt, pero necesitaría usar una sección mutex/crítica adecuada para bloquear y marcar "pObj == NULL" antes y después del bloqueo.Por supuesto, pObj también tendría que ser estático;).Un mutex sería innecesariamente pesado en este caso, sería mejor optar por una sección crítica.

DO, eso no funciona.Como señaló Chris, eso es un bloqueo de doble verificación, que no se garantiza que funcione en el estándar actual de C++.Ver: C++ y los peligros del bloqueo doblemente verificado

Editar:No hay problema, DO.Es realmente bueno en idiomas donde funciona.Espero que funcione en C++0x (aunque no estoy seguro), porque es un modismo muy conveniente.

  1. leer en el modelo de memoria débil.Puede romper cerraduras y cerraduras giratorias doblemente revisadas.Intel es un modelo de memoria potente (todavía), por lo que en Intel es más fácil

  2. utilice con cuidado "volatile" para evitar el almacenamiento en caché de partes del objeto en los registros; de lo contrario, habrá inicializado el puntero del objeto, pero no el objeto en sí, y el otro hilo se bloqueará.

  3. El orden de inicialización de variables estáticas frente a la carga de código compartido a veces no es trivial.He visto casos en los que el código para destruir un objeto ya estaba descargado, por lo que el programa fallaba al salir.

  4. Estos objetos son difíciles de destruir adecuadamente.

En general, los singletons son difíciles de hacer bien y difíciles de depurar.Es mejor evitarlos por completo.

Supongo que decir no hagas esto porque no es seguro y probablemente se romperá con más frecuencia que simplemente inicializar estas cosas en main() no va a ser tan popular.

(Y sí, sé que sugerir eso significa que no deberías intentar hacer cosas interesantes en constructores de objetos globales.Ese es el punto.)

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top