Итак, я видел много статей, в которых утверждается, что в C ++ блокировка с двойной проверкой, обычно используемая для предотвращения попытки нескольких потоков инициализировать лениво созданный синглтон, не работает. Обычный код блокировки с двойной проверкой выглядит следующим образом:
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;
}
};
Проблема, очевидно, заключается в экземпляре назначения строки - компилятор может свободно выделить объект и затем назначить ему указатель, ИЛИ установить указатель на то, где он будет размещен, а затем выделить его. В последнем случае нарушается идиома - один поток может выделить память и назначить указатель, но не запускать конструктор синглтона до его перевода в спящий режим - тогда второй поток увидит, что экземпляр не равен нулю, и попытается вернуть его хотя он еще не построен.
Я увидел предложение использовать локальный логический поток и проверить это вместо instance
. Примерно так:
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;
}
};
Таким образом, каждый поток завершает проверку, был ли экземпляр создан один раз, но останавливается после этого, что влечет за собой некоторое снижение производительности, но все же не так плохо, как блокировка каждого вызова. Но что, если мы просто использовали локальный статический тип bool?:
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;
}
};
Почему бы это не сработало? Даже если sync_check должен был быть прочитан одним потоком, когда он назначается в другом, значение мусора все равно будет отличным от нуля и, следовательно, верным. В этой статье доктора Добба утверждается, что вам нужно блокировать, потому что вы никогда не выиграете битву с компилятором за инструкции по переупорядочению. Что заставляет меня думать, что это не должно работать по какой-то причине, но я не могу понять, почему. Если требования к точкам последовательности столь же потеряны, как заставляет меня верить статья доктора Добба, я не понимаю, почему любой код после блокировки не может быть переупорядочен до блокировки. Что сделало бы многопоточность C ++ прерывистым периодом.
Полагаю, я мог видеть, что компилятору разрешено специально переупорядочивать sync_check до блокировки, потому что это локальная переменная (и хотя она статическая, мы не возвращаем ссылку или указатель на нее) - но тогда это может все еще можно решить, сделав вместо этого статический член (фактически глобальный).
Так это будет работать или нет? Почему?