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é?
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
Gracias, corregido. Había olvidado completamente la condición de carrera. –
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