5

Итак, я видел много статей, в которых утверждается, что на 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 до блокировки, потому что это локальная переменная (и хотя она статична, мы не возвращаем ссылку или указатель на нее), но тогда это все же можно решить, сделав вместо этого статический член (фактически глобальный).

Так будет ли это работать, или нет? Зачем?

+2

Проблема в том, что переменная может быть назначена до того, как конструктор будет запущен (или завершен), а не до выделения объекта. – kdgregory

+0

Спасибо, исправлено. Я полностью забыл состояние гонки. –

+1

Да, вы правы, текущий C++ действительно является «многопоточным сломанным периодом». при рассмотрении стандарт только. Поставщики компиляторов обычно предоставляют способы обойти это, поэтому практические результаты не так уж ужасны. – Suma

ответ

5

Ваше исправление ничего не исправляет, поскольку запись на sync_check и экземпляр может быть выведен из строя на CPU. В качестве примера представьте, что первые два вызова экземпляра происходят примерно в одно и то же время на двух разных процессорах. Первый поток получит блокировку, инициализирует указатель и устанавливает значение sync_check в true, в этом порядке, но процессор может изменить порядок записи в память. На другом CPU тогда, когда второй поток может проверить sync_check, посмотрите, что это правда, но экземпляр еще не может быть записан в память. См. Lockless Programming Considerations for Xbox 360 and Microsoft Windows.

Определяемое по конкретным потокам решение sync_check, которое вы упомянули, должно работать тогда (при условии, что вы инициализируете свой указатель на 0).

+0

Что касается вашего последнего предложения: Да, но я не уверен, но я думаю, что thread_specific_ptr использует мьютекс внутри себя. Итак, какой смысл использовать это решение или просто блокировать мьютекс (без двойной блокировки)? – n1ckp

1

Там какая-то большое чтение об этом (хотя это .net/C# ориентированный) здесь: http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

Что это сводится к тому, что вы должны быть в состоянии сказать процессору, что он не может изменить порядок чтения/записи для этого доступа с переменной скоростью (с момента первоначального Pentium, CPU может изменить порядок определенных инструкций, если он думает, что логика не будет затронута), и что ему необходимо обеспечить, чтобы кеш был согласован (не забывайте об этом - мы разработчики что вся память - это всего лишь один плоский ресурс, но на самом деле у каждого ядра ЦП есть кеш, некоторые не разделенные (L1), иногда некоторые могут быть разделены (L2)) - ваша инициализация может записываться в основную RAM, но другое ядро может иметь неинициализированное значение в кеше. Если у вас нет семантики параллелизма, CPU может не знать, что это кеш грязный.

Я не знаю сторону C++, но в .net вы бы указали переменную как энергозависимую, чтобы защитить доступ к ней (или вы будете использовать методы чтения/записи памяти в System.Threading).

Как в стороне, я прочитал, что в .net 2.0 гарантируется, что двойная проверенная блокировка будет работать без «изменчивых» переменных (для любых читателей .net там) - это не поможет вам с вашим C++ код.

Если вы хотите быть в безопасности, вам нужно будет сделать эквивалент C++ для обозначения переменной как изменчивой в C#.

+1

Переменные C++ могут быть объявлены как изменчивые, но я сомневаюсь, что это имеет ту же семантику, что и C#. Я также помню, где-то читал, что это злоупотребление волатильностью, но я не помню, почему поэтому я не могу судить, насколько аргументирована статья. –

+0

На разных языках это может быть злоупотребление (может даже быть злоупотреблением в C#). Одним из действительно трудных аспектов написания кода с низкой блокировкой или блокировки является несоответствие в руководстве. Я потратил время на чтение, и кажется, что даже внутри Microsoft некоторые блоггеры, похоже, противоречат друг другу, когда вам нужен забор памяти, и когда вы должны использовать volatile. Конечно, это сложная проблема. – JMarsch

+0

Нет эквивалента .NET volatile в текущем C++ (как определено стандартом). Это один из направлений, который принесет новый стандарт C++ 0x. Тем временем вам нужно использовать то, что предлагает ваш корреспондент (что в Visual Studio означает неустойчивость и забор памяти). – Suma

0

«Последний случай разрушает идиому - две нити могут создать синглтон».

Но если я правильно понял код, то в первом примере вы проверяете, существует ли экземпляр (может выполняться одновременно несколькими потоками), если он не имеет одного потока, чтобы заблокировать его, и он создает экземпляр - только один поток может выполнить создание в то время. Все остальные потоки блокируются и будут ждать.

После того, как экземпляр создан и мьютекс разблокирован, следующий ожидающий поток блокирует мьютексы, но он не будет пытаться создать новый экземпляр, потому что проверка завершится неудачно.

В следующий раз, когда проверяется переменная экземпляра, она будет установлена ​​так, чтобы нитки не пытались создать новый экземпляр.

Я не уверен в том, что один поток назначает новый экземпляр экземпляра, тогда как другой поток проверяет одну и ту же переменную, но я считаю, что в этом случае он будет корректно обрабатываться.

Я что-то упустил?

Не уверен в переупорядочивании операций, но в этом случае это изменило бы логику, поэтому я не ожидал, что это произойдет - но я не эксперт по этой теме.

+0

Вы правы - я ошибался относительно фактического состояния гонки. Проблема состоит в том, что второй поток может видеть, что экземпляр не равен null и пытается вернуть его до того, как его построил первый поток. Я редактировал свой пост. –

 Смежные вопросы

  • Нет связанных вопросов^_^