Необходимость волатильного модификатора в двойной проверенной блокировке в .NET.

В нескольких текстах сказано, что при реализации блокировки с двойным проверкой в ​​.NET в поле, в котором вы блокируетесь, должен применяться изменчивый модификатор. Но почему именно? Учитывая следующий пример:

public sealed class Singleton { private static volatile Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Singleton(); } } return instance; } } } 

почему «lock (syncRoot)» не выполняет необходимую последовательность памяти? Не правда ли, что после слова «блокировка» как чтение, так и запись были бы неустойчивыми, и поэтому была бы обеспечена необходимая согласованность?

Летучие не нужны. Ну, вроде **

volatile используется для создания барьера памяти * между чтением и записью переменной.
lock , когда используется, вызывает создание барьеров памяти вокруг блока внутри lock , в дополнение к ограничению доступа к блоку к одному streamу.
Пункты памяти делают так, чтобы каждый stream считывал самое текущее значение переменной (не локальное значение, кэшированное в каком-то регистре), и что компилятор не переупорядочивает утверждения. Использование volatile не требуется **, потому что у вас уже есть блокировка.

Джозеф Альбахари объясняет этот материал лучше, чем когда-либо.

И не забудьте проверить руководство Джона Скита по внедрению синглтона в C #

обновление :
* volatile вызывает чтение переменной, которая должна быть VolatileRead s, а записи – VolatileWrite s, которые на x86 и x64 на CLR реализованы с помощью MemoryBarrier . Они могут быть более мелкозернистыми на других системах.

** мой ответ верен только в том случае, если вы используете CLR для процессоров x86 и x64. Это может быть правдой в других моделях памяти, таких как Mono (и другие реализации), Itanium64 и будущие аппаратные средства. Это то, о чем говорит Джон в своей статье в «gotchas» для двойной проверки блокировки.

Для Thread.MemoryBarrier функционирования кода в ситуации модели с слабой памятью может потребоваться сделать одну из {маркировки переменной как volatile , прочитав ее с помощью Thread.VolatileRead или вставив вызов Thread.MemoryBarrier }.

Из того, что я понимаю, на CLR (даже на IA64) записи никогда не переупорядочиваются (у записей всегда есть семантика выпуска). Однако, на IA64, чтение может быть переупорядочено, чтобы прибыть перед записью, если они не маркированы volatile. К сожалению, у меня нет доступа к оборудованию IA64 для игры, поэтому все, что я говорю об этом, было бы предположением.

Я также нашел эти статьи полезными:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
vance morrison (все ссылки на это, речь идет о двойной проверенной блокировке)
статья Криса Брумме (все ссылки на это)
Джо Даффи: Сломанные варианты двойного проверенного блокирования

Серия luis abreu по многопоточности дает хороший обзор концепций
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx

Существует способ реализовать его без volatile поля. Я объясню это …

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

 public sealed class Singleton { private static Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { // very fast test, without implicit memory barriers or locks if (instance == null) { lock (syncRoot) { if (instance == null) { var temp = new Singleton(); // ensures that the instance is well initialized, // and only then, it assigns the static variable. System.Threading.Thread.MemoryBarrier(); instance = temp; } } } return instance; } } } 

Понимание кода

Представьте, что внутри конструктора classа Singleton есть код инициализации. Если эти инструкции переупорядочиваются после того, как поле задано с адресом нового объекта, то у вас есть неполный экземпляр … представьте, что class имеет этот код:

 private int _value; public int Value { get { return this._value; } } private Singleton() { this._value = 1; } 

Теперь представьте себе вызов конструктора с использованием нового оператора:

 instance = new Singleton(); 

Это можно расширить до следующих операций:

 ptr = allocate memory for Singleton; set ptr._value to 1; set Singleton.instance to ptr; 

Что делать, если я переупорядочу эти инструкции следующим образом:

 ptr = allocate memory for Singleton; set Singleton.instance to ptr; set ptr._value to 1; 

Есть ли разница? НЕТ, если вы думаете об одном streamе. ДА, если вы думаете о нескольких streamах … что, если stream прерывается сразу после set instance to ptr :

 ptr = allocate memory for Singleton; set Singleton.instance to ptr; -- thread interruped here, this can happen inside a lock -- set ptr._value to 1; -- Singleton.instance is not completelly initialized 

Это то, что предотвращает барьер памяти, не позволяя переупорядочивать доступ к памяти:

 ptr = allocate memory for Singleton; set temp to ptr; // temp is a local variable (that is important) set ptr._value to 1; -- memory barrier... cannot reorder writes after this point, or reads before it -- -- Singleton.instance is still null -- set Singleton.instance to temp; 

Счастливое кодирование!

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

Неустойчивый и первый if (instance == null) не являются «необходимыми». Блокировка сделает этот код streamобезопасным.

Поэтому возникает вопрос: зачем вы добавляете первый if (instance == null) ?

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

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

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

Таким образом, вы делаете singleton volatile, чтобы убедиться, что вы читаете последнее значение, без необходимости использовать блокировку.

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

Теперь, это действительно полезно?

Ну, я бы сказал «в большинстве случаев нет».

Если Singleton.Instance может вызвать неэффективность из-за блокировок, то почему вы так часто называете это, что это будет серьезной проблемой ? Вся суть одноэлементности заключается в том, что есть только один, поэтому ваш код может читать и кэшировать одноэлементную ссылку один раз.

Единственный случай, когда я могу думать о том, где это кэширование будет невозможным, – это когда у вас есть большое количество streamов (например, сервер, использующий новый stream для обработки каждого запроса, может создавать миллионы очень коротких streamов, каждый из которых который должен был бы вызвать Singleton.Instance один раз).

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

AFAIK (и – принимайте это с осторожностью, я не делаю много параллельных вещей) нет. Блокировка просто дает вам синхронизацию между несколькими соперниками (streamами).

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

См. http://msdn.microsoft.com/en-us/library/ms998558.aspx и обратите внимание на следующую цитату:

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

Описание волатильности: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx

Вы должны использовать volatile с шаблоном двойной проверки.

Большинство людей указывают на эту статью как доказательство того, что вам не нужна летучая способность: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

Но они не читают до конца: « Окончательное слово предупреждения – я только догадываюсь о модели памяти x86 от наблюдаемого поведения на существующих процессорах. Таким образом, методы с низким уровнем блокировки также являются хрупкими, поскольку аппаратные средства и компиляторы могут стать более агрессивными с течением времени Ниже приведены некоторые страtagsи, позволяющие свести к минимуму влияние этой хрупкости на ваш код. Во-первых, по возможности избегайте методов с низким уровнем блокировки. (…) Наконец, предположим, что самая слабая модель памяти возможна, используя изменчивые декларации, вместо того, чтобы полагаться на неявные гарантии «.

Если вам нужно более убедительно, тогда прочитайте эту статью о спецификации ECMA, которая будет использоваться для других платформ: msdn.microsoft.com/en-us/magazine/jj863136.aspx

Если вам нужно еще больше убедить, прочитайте эту новую статью о том, что можно оптимизировать оптимизацию, чтобы она не работала без изменчивости: msdn.microsoft.com/en-us/magazine/jj883956.aspx

Таким образом, это может «работать» для вас без изменчивости на данный момент, но не помешает ему написать правильный код и либо использовать изменчивые, либо методы volatileread / write. Статьи, которые предлагают сделать иначе, иногда оставляют некоторые из возможных рисков оптимизации JIT / компилятора, которые могут повлиять на ваш код, а также будущие оптимизации, которые могут произойти, что может сломать ваш код. Кроме того, как упоминалось в предыдущей статье, предположения о работе без волатильности уже не могут иметь место в ARM.

lock достаточно. Специфика языка MS (3.0) сама упоминает этот точный сценарий в §8.12 без упоминания об volatile :

Лучшим подходом является синхронизация доступа к статическим данным путем блокировки частного статического объекта. Например:

 class Cache { private static object synchronizationObject = new object(); public static void Add(object x) { lock (Cache.synchronizationObject) { ... } } public static void Remove(object x) { lock (Cache.synchronizationObject) { ... } } } 

Я думаю, что нашел то, что искал. Подробности приведены в этой статье – http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10 .

Подводя итог – в .NET volatile модификатор действительно не нужен в этой ситуации. Однако при более слабых моделях памяти записи, сделанные в конструкторе лениво инициированного объекта, могут быть отложены после записи в поле, поэтому другие streamи могут читать коррумпированный ненулевой экземпляр в первом операторе if.

Это довольно хороший пост об использовании летучих с двойной проверенной блокировкой:

http://tech.puredanger.com/2007/06/15/double-checked-locking/

В Java, если целью является защита переменной, вам не нужно блокировать, если она помечена как изменчивая

  • Почему существует летучесть?
  • Неустойчивые гарантии и исполнение вне порядка
  • Interesting Posts

    Получение состояния переменных после ошибки в R

    Используя Protobuf-net, я внезапно получил исключение из-за неизвестного типа провода

    Получают ли данные memtest86 дефектный модуль памяти?

    Является ли transferCurrentComplicationUserInfo более подходящим для обновления осложнений?

    PowerShell говорит, что «выполнение скриптов отключено в этой системе».

    Какие жесткие диски совместимы с ноутбуками?

    Закаленный жадный токен – что отличает размещение точки до негативного взгляда

    Ошибка выполнения приложения: processDebugResources Android Studio

    взяв адрес временного объекта

    Node.js, невозможно открыть файлы. Ошибка: ENOENT, stat ‘./path/to/file’

    фильтр для полных случаев в data.frame с использованием dplyr (случайное удаление)

    rails paperclip и пассажир `не распознается командой ‘ident’

    Как создать закладки PDF из Microsoft Word?

    Как извлечь ключ Win 8 OEM, встроенный в BIOS?

    Регулятор громкости в приложении android

    Давайте будем гением компьютера.