Итак, я видел много статей, в которых утверждается, что на 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;
}
};
Проблема, видимо, является назначение экземпляра строки - компилятор волен выделить объект, а затем присвоить указатель на него, или установить указатель туда, где его будет выделено, а затем распределить его. Последний случай разрушает идиому - один поток может выделять память и назначать указатель, но не запускать конструктор singleton, прежде чем он будет усыпан, - тогда второй поток увидит, что экземпляр не равен нулю и попытается вернуть его , хотя он еще не построен.
I saw a suggestion использовать поток local boolean и проверить, что вместо 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 должен был быть прочитан одним потоком, когда он назначается в другом, значение мусора по-прежнему будет отличным от нуля и, следовательно, истинным. This Dr. Dobb's article утверждает, что вам нужно заблокировать, потому что вы никогда не выиграете битву с компилятором над инструкциями по переупорядочению. Это заставляет меня думать, что это не должно работать по какой-то причине, но я не могу понять, почему. Если требования к точкам последовательности так же теряются, как и статья доктора Добба, я не понимаю, почему любой код после того, как блокировка не может быть переупорядочена до блокировки. Это сделает C++ многопоточным сломанным периодом.
Я думаю, что я мог видеть, что компилятору разрешено специально переупорядочить sync_check до блокировки, потому что это локальная переменная (и хотя она статична, мы не возвращаем ссылку или указатель на нее), но тогда это все же можно решить, сделав вместо этого статический член (фактически глобальный).
Так будет ли это работать, или нет? Зачем?
Проблема в том, что переменная может быть назначена до того, как конструктор будет запущен (или завершен), а не до выделения объекта. – kdgregory
Спасибо, исправлено. Я полностью забыл состояние гонки. –
Да, вы правы, текущий C++ действительно является «многопоточным сломанным периодом». при рассмотрении стандарт только. Поставщики компиляторов обычно предоставляют способы обойти это, поэтому практические результаты не так уж ужасны. – Suma