Enlazar la construcción floja y segura de un singleton en C ++

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

  1. Perezosamente construido de una manera segura para hilos (dos hilos podrían ser al mismo tiempo el primer usuario del singleton, aún así solo debería construirse una vez).
  2. No se basa en variables estáticas que se construyan de antemano (por lo que el objeto singleton es seguro de usar durante la construcción de variables estáticas).

(No conozco mi C ++ suficientemente bien, pero es el caso de que las variables estáticas integrales y constantes se inicialicen antes de que se ejecute cualquier código (es decir, incluso antes de que se ejecuten constructores estáticos, es posible que sus valores ya estén “inicializados” en el progtwig imagen)? En caso afirmativo, tal vez esto se puede aprovechar para implementar un mutex singleton, que a su vez se puede utilizar para proteger la creación del singleton real …


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

  1. Use la inicialización estática (en oposición a la inicialización dinámica) de una variable estática POD e implemente mi propio mutex con eso usando las instrucciones atómicas incorporadas. Este era el tipo de solución que estaba insinuando en mi pregunta, y creo que ya lo sabía.
  2. Utilice alguna otra función de biblioteca como pthread_once o boost :: call_once . Estos ciertamente no los conocía, y estoy muy agradecido por las respuestas publicadas.

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

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

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

Los objetos con una duración de almacenamiento estática (3.7.1) se inicializarán en cero (8.5) antes de que se lleve a cabo cualquier otra inicialización. La inicialización cero y la inicialización con una expresión constante se denominan colectivamente inicialización estática; el rest de la inicialización es inicialización dinámica. 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 ámbito de 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 sabe que va a utilizar este singleton durante la inicialización de otros objetos estáticos, creo que encontrará que la sincronización no es un problema. Según mi leal saber y entender, todos los comstackdores principales inicializan objetos estáticos en un único hilo, por lo que la seguridad de los hilos durante la inicialización estática. Puede declarar que su puntero único es NULO, y luego verificar para ver si se ha inicializado antes de usarlo.

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

Editar: la sugerencia de Chris de usar una comparación y cambio atómicos 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 de gastos generales ligeramente menor.

Desafortunadamente, la respuesta de Matt presenta lo que se llama locking de doble verificación 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 con .NET). Esto significa que, entre el momento en que se realiza la comprobación pObj == NULL y el momento en que se adquiere el locking (mutex), pObj ya puede tener sido asignado en otro hilo. La conmutación de subprocesos ocurre cada vez que el sistema operativo lo desea, no entre “líneas” de un progtwig (que no tienen significado para la comstackción posterior en la mayoría de los lenguajes).

Además, como Matt reconoce, utiliza un int como un locking en lugar de una primitiva del sistema operativo. No hagas eso. Los lockings adecuados requieren el uso de instrucciones de barrera de memoria, potencialmente vaciados de línea de caché, y así sucesivamente; usa las primitivas de tu 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 podría no funcionar en la CPU Foo2. La mayoría de los sistemas operativos admiten nativamente los hilos POSIX (subprocesos) o los ofrecen como un contenedor para el paquete de subprocesamiento del sistema operativo, por lo que a menudo es mejor ilustrar los ejemplos que los utilizan.

Si su sistema operativo ofrece las primitivas adecuadas, y si realmente lo necesita para el rendimiento, en lugar de realizar este tipo de locking / inicialización, puede utilizar una operación atómica de comparación e intercambio para inicializar una variable global compartida. Esencialmente, lo que escribes 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 cada hebra que invoque a GetSingleton () simultáneamente) y luego eliminar los extras. La función OSAtomicCompareAndSwapPtrBarrier proporcionada en Mac OS X, la mayoría de los sistemas operativos proporcionan una primitiva similar, comprueba si pObj es NULL y solo lo establece como temp si es así. Esto usa soporte de hardware para realmente, literalmente, solo realizar el intercambio una vez y decir si sucedió.

Otra facilidad para aprovechar si su sistema operativo lo ofrece que está entre estos dos extremos es pthread_once . Esto le permite configurar una función que se ejecuta una sola vez, básicamente haciendo todo el locking / barrera / etc. engaño para usted, no importa cuántas veces se invoque o en cuántos hilos se invoque.

Aquí hay un getter singleton muy simple y perezosamente construido:

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

Esto es flojo, 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 una manera segura. Entonces, si ese es su comstackdor de destino o si usa un comstackdor que también lo implementa de manera segura (tal vez lo hagan los comstackdores de Visual Studio más nuevos), entonces esto podría ser todo lo que necesita.

También vea 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 Boost.Thread para este fin. Lea la sección “inicialización única” para obtener más información.

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

Para gcc, esto es bastante fácil:

 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 reiniciar el LazyType a uno nuevo entre pruebas, o si desea cambiar el LazyType * a un MockLazyType *, no podrá hacerlo. Dado esto, generalmente es mejor usar un mutex estático + puntero estático.

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

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

  • Si desea crear instancias vagas del singleton mientras utiliza 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 el locking y marcando “pObj == NULL” antes y después del locking. Por supuesto, pObj también debería ser estático ;). Un mutex sería innecesariamente pesado en este caso, sería mejor ir con una sección crítica.

Pero como ya se dijo, no se puede garantizar la inicialización diferida insegura sin usar al menos una primitiva de sincronización.

Editar: Sí, Derek, tienes razón. Mi error. 🙂

Podría usar la solución de Matt, pero necesitaría usar una sección mutex / crítica adecuada para el locking y marcando “pObj == NULL” antes y después del locking. Por supuesto, pObj también debería ser estático;). Un mutex sería innecesariamente pesado en este caso, sería mejor ir con una sección crítica.

OJ, eso no funciona. Como señaló Chris, eso es un locking de doble verificación, que no está garantizado para funcionar en el estándar actual de C ++. Ver: C ++ y los peligros del locking doblemente controlado

Editar: No hay problema, OJ. Es realmente bueno en idiomas donde funciona. Espero que funcione en C ++ 0x (aunque no estoy seguro), porque es una expresión tan conveniente.

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

  2. utilice cuidadosamente “volátil” para evitar el almacenamiento en caché de las partes del objeto en los registros; de lo contrario, habrá inicializado el puntero del objeto, pero no el objeto en sí, y el otro subproceso 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 progtwig se bloqueó al salir

  4. tales objetos son difíciles de destruir apropiadamente

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

Supongo que decir que no hagas esto porque no es seguro y probablemente se rompa con más frecuencia que solo inicializar esto en main() no será tan popular.

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