c++ multithreading singleton construction lazy-initialization

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



multithreading construction (10)

Podría usar la solución de Matt, pero necesitaría usar una sección mutex / crítica adecuada para el bloqueo y marcando "pObj == NULL" antes y después del bloqueo. 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 bloqueo de doble verificación, que no está garantizado para funcionar en el estándar actual de C ++. Ver: C ++ y los peligros del bloqueo 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.

¿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 construidas 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 que las variables estáticas integrales y constantes se inicializan antes de que se ejecute cualquier código (es decir, incluso antes de que se ejecuten constructores estáticos, sus valores pueden ya estar "inicializados" en el programa 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 resto 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 compiladores 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 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.


No puede hacerlo sin variables estáticas, sin embargo, si está dispuesto a tolerar una, puede usar Boost.Thread para este propósito. 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.


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

  • Si desea una instanciación lenta 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 bloqueo y marcando "pObj == NULL" antes y después del bloqueo. 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. :)


@Estera

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.]

MSN

No está tratando de hacer nada interesante en un constructor de objetos global. Está tratando de evitar hacer algo interesante, retrasando la inicialización hasta más tarde (construcción perezosa). Inicializar una variable a 0 e inicializar un mutex con un inicializador estático no cuentan como interesantes, porque ambas son cosas que el compilador puede hacer.


Desafortunadamente, la respuesta de Matt presenta lo que se llama bloqueo de doble verificación que no es compatible con el modelo de memoria C / C ++. (Es compatible con Java 1.5 y posterior, y creo que con el modelo de memoria .NET). Esto significa que, entre el momento en que se realiza la comprobación pObj == NULL y el momento en que se adquiere el bloqueo (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 programa (que no tienen significado para la compilación posterior en la mayoría de los lenguajes).

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í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 subprocesos 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 bloqueo / 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 bloqueo / barrera / etc. engaño para usted, no importa cuántas veces se invoque o en cuántos hilos se invoque.


  1. leer en el modelo de memoria débil. Puede romper cerraduras y espinilleras doblemente controladas. Intel es un modelo de memoria fuerte (aún), 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 programa 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.


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 compilador de destino o si usa un compilador que también lo implementa de manera segura (tal vez lo hagan los compiladores 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.


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 programas 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.


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).