5

Así que he visto muchos artículos que ahora afirman que en C++ el bloqueo comprobado doble, comúnmente utilizado para evitar que varios hilos intenten inicializar un singleton creado perezosamente, está roto. código de bloqueo comprobado normal doble lee así:¿Qué pasa con esta solución para el bloqueo comprobado doble?

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 

public: 
    static singleton & instance() 
    { 
     static singleton* instance; 

     if(!instance) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 
     } 

     return *instance; 
    } 
}; 

El problema parece ser que es la instancia asignación de línea - el compilador es libre de asignar el objeto y luego asignar el puntero a ella, o para ajustar la indicación del lugar donde se se asignará, luego asignarlo. El último caso rompe la expresión idiomática: un hilo puede asignar la memoria y asignar el puntero pero no ejecutar el constructor del singleton antes de que se ponga en suspensión, luego el segundo subproceso verá que la instancia no es nula e intentará devolverlo , a pesar de que aún no ha sido construido.

I saw a suggestion para utilizar un hilo local booleano y compruebe que en lugar de instance. Algo como esto:

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 
    static boost::thread_specific_ptr<int> _sync_check; 

public: 
    static singleton & instance() 
    { 
     static singleton* instance; 

     if(!_sync_check.get()) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 

      // Any non-null value would work, we're really just using it as a 
      // thread specific bool. 
      _sync_check = reinterpret_cast<int*>(1); 
     } 

     return *instance; 
    } 
}; 

De esta manera cada hilo termina de comprobar si la instancia se ha creado una vez, pero se detiene después de eso, lo que implica un poco de impacto en el rendimiento, pero aún no casi tan malo como bloquear todas las llamadas. Pero, ¿y si acabamos de utilizar un bool estático local ?:

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 

public: 
    static singleton & instance() 
    { 
     static bool sync_check = false; 
     static singleton* instance; 

     if(!sync_check) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 

      sync_check = true; 
     } 

     return *instance; 
    } 
}; 

¿Por qué no funcionaría? Incluso si un subproceso debe leer sync_check cuando está siendo asignado en otro, el valor de la basura seguirá siendo distinto de cero y, por lo tanto, verdadero. This Dr. Dobb's article afirma que tiene que bloquear porque nunca ganará una batalla con el compilador sobre las instrucciones de reordenamiento. Lo que me hace pensar que esto no debe funcionar por alguna razón, pero no puedo entender por qué. Si los requisitos de los puntos de secuencia son tan bajos como me hace creer el artículo del Dr. Dobb, no entiendo por qué el código después del bloqueo no se pudo reordenar antes del bloqueo. Lo que haría que C++ multihilo con período roto.

Supongo que podría ver que el compilador reordena específicamente sync_check antes del bloqueo porque es una variable local (y aunque es estático no le estamos devolviendo una referencia o un puntero), pero entonces este aún podría resolverse convirtiéndolo en un miembro estático (efectivamente global) en su lugar.

¿Funcionará o no? ¿Por qué?

+2

El problema es que la variable se puede asignar antes de que el constructor se ejecute (o complete), no antes de asignar el objeto. – kdgregory

+0

Gracias, corregido. Había olvidado completamente la condición de carrera. –

+1

Sí, está en lo correcto, C++ actual realmente es "período roto multiproceso". cuando se considera solo estándar. Sin embargo, los vendedores de compiladores siempre ofrecen formas de evitar esto, por lo tanto, los resultados prácticos no son tan terribles. – Suma

Respuesta

5

Su reparación no soluciona nada ya que las escrituras en sync_check y la instancia pueden estar desordenadas en la CPU. Como ejemplo, imagine que las dos primeras llamadas a instancia ocurren aproximadamente al mismo tiempo en dos CPU diferentes. El primer hilo adquirirá el bloqueo, inicializará el puntero y establecerá sync_check en verdadero, en ese orden, pero el procesador puede cambiar el orden de las escrituras en la memoria. En la otra CPU, entonces es posible que el segundo subproceso verifique sync_check, vea que es verdadero, pero es posible que todavía no se haya escrito en la memoria. Vea Lockless Programming Considerations for Xbox 360 and Microsoft Windows para más detalles.

La solución de comprobación sync_check específica para el hilo que mencione debería funcionar entonces (suponiendo que inicialice su puntero a 0).

+0

Acerca de tu última oración: Sí, pero no estoy seguro, pero creo que thread_specific_ptr usa un mutex internamente. Entonces, ¿cuál sería el sentido de usar esa solución en lugar de simplemente bloquear el mutex (sin doble bloqueo)? – n1ckp

1

Hay un gran lectura sobre esto (aunque es .NET/C# orientado) aquí: http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

Lo que se reduce a es que se necesita para ser capaz de decirle a la CPU que no puede cambiar el orden de su lecturas/escrituras para este acceso variable (desde el Pentium original, la CPU puede reordenar ciertas instrucciones si cree que la lógica no se vería afectada), y que necesita asegurarse de que la caché sea coherente (no se olvide de eso - nosotros devs llegar a pretender que toda la memoria es solo un recurso plano, pero en realidad, cada núcleo de CPU tiene caché, algunos no compartidos (L1), algunos pueden compartirse algunas veces (L2) - tu iniciación podría escribir en la RAM principal, pero otro núcleo podría tener el valor no inicializado en la memoria caché. Si no tiene ninguna semántica de simultaneidad, la CPU puede no saber que su caché está sucia.

No conozco el lado de C++, pero en .net, designaría la variable como volátil para proteger el acceso a ella (o utilizaría los métodos de barrera de lectura/escritura de memoria en System.Threading).

Como nota aparte, he leído que en .net 2.0, el bloqueo verificado doblemente garantiza que funciona sin variables "volátiles" (para cualquier lector de .net) - eso no lo ayuda con su C++ código.

Si quiere estar seguro, tendrá que hacer el equivalente en C++ de marcar una variable como volátil en C#.

+1

Las variables de C++ se pueden declarar como volátiles, pero dudo que tengan exactamente la misma semántica que C#. También recuerdo haber leído en alguna parte que se trataba de un abuso de sustancias volátiles, pero no recuerdo por qué, así que no puedo juzgar qué tan razonado fue el artículo. –

+0

En diferentes idiomas, podría ser un abuso (incluso podría ser un abuso en C#). Uno de los aspectos realmente difíciles de escribir bajo código de bloqueo o bloqueo ha sido la disparidad en la orientación. He dedicado tiempo a leer sobre esto, y parece que incluso dentro de Microsoft, algunos de los bloggers parecen contradecirse entre sí cuando necesitas una valla de memoria y cuando deberías usar volátiles. Es un problema difícil, para estar seguro. – JMarsch

+0

No hay equivalente de .NET volátil en C++ actual (como se define en el estándar). Es una de las áreas que presentará el estándar C++ 0x. Mientras tanto, necesitas usar lo que ofrece tu compilador (que en Visual Studio significa valla volátil y de memoria). – Suma

0

"El último caso rompe el modismo: dos hilos podrían terminar creando el singleton".

Pero si entiendo el código correctamente, el primer ejemplo, comprueba si la instancia ya existe (puede ser ejecutada por varios subprocesos al mismo tiempo), si no tiene un thread get's para bloquearla y crea el instancia: solo un hilo puede ejecutar la creación en ese momento. Todos los demás hilos quedan bloqueados y esperan.

Una vez que se crea la instancia y se desbloquea el mutex, el siguiente hilo de espera bloqueará mutex pero no intentará crear una nueva instancia porque la verificación fallará.

La próxima vez que se marque la variable de instancia, se establecerá para que ningún subproceso intente crear una nueva instancia.

No estoy seguro del caso en que un hilo está asignando un nuevo puntero a instancia mientras que otro hilo comprueba la misma variable, pero creo que se manejará correctamente en este caso.

¿Falta algo aquí?

Ok no estoy seguro sobre el reordenamiento de las operaciones, pero en este caso estaría alterando la lógica, así que no esperaría que sucediera, pero no soy un experto en este tema.

+0

Tienes razón, estaba equivocado sobre la condición real de carrera. El problema es que un segundo hilo puede ver que la instancia no es nula e intenta devolverlo antes de que el primer hilo lo haya construido. Edité mi publicación. –

Cuestiones relacionadas