В нескольких текстах говорится, что при реализации блокировки с двойной проверкой в .NET к блокируемому полю должен применяться модификатор volatile. Но почему именно? Рассмотрим следующий пример:
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)» не обеспечивает необходимой согласованности памяти? Разве не правда, что после оператора «lock» и чтение, и запись будут непостоянными, и поэтому необходимая согласованность будет достигнута?
Ответы:
Неустойчивый не нужен. Ну вроде **
volatile
используется для создания барьера памяти * между чтением и записью переменной.lock
при использовании вызывает создание барьеров памяти вокруг блока внутриlock
, в дополнение к ограничению доступа к блоку одним потоком.Барьеры памяти делают так, чтобы каждый поток считывал самое текущее значение переменной (а не локальное значение, кэшированное в каком-либо регистре), и что компилятор не меняет порядок операторов. Использование
volatile
не требуется **, потому что у вас уже есть блокировка.Джозеф Альбахари объясняет этот материал лучше, чем я когда-либо мог.
И обязательно ознакомьтесь с руководством Джона Скита по реализации синглтона на C #.
update :
*
volatile
вызывает чтение переменнойVolatileRead
s, а записьVolatileWrite
s, которые на x86 и x64 в CLR реализованы с расширениемMemoryBarrier
. В других системах они могут быть более мелкими.** мой ответ правильный, только если вы используете CLR на процессорах x86 и x64. Это может быть верно для других моделей памяти, например, для Mono (и других реализаций), Itanium64 и будущего оборудования. Это то, что имеет в виду Джон в своей статье «Попытки» для блокировки с двойной проверкой.
Выполнение одного из следующих действий: {отметка переменной как
volatile
, ее чтениеThread.VolatileRead
или вставка вызоваThread.MemoryBarrier
} может потребоваться для правильной работы кода в ситуации слабой модели памяти.Насколько я понимаю, в CLR (даже на IA64) записи никогда не переупорядочиваются (записи всегда имеют семантику выпуска). Однако в IA64 операции чтения могут быть переупорядочены так, чтобы они выполнялись перед записью, если они не помечены как изменчивые. К сожалению, у меня нет доступа к оборудованию IA64, с которым можно поиграть, поэтому все, что я говорю об этом, будет лишь домыслом.
Я также нашел эти статьи полезными:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
статья
ванса Моррисона (все ссылки на это, в нем говорится о блокировке с двойной проверкой) статья Криса Брумма (все ссылки на это )
Джо Даффи: сломанные варианты блокировки с двойной проверкой
Серия статей Луиса Абреу о многопоточности также дает хороший обзор концепций
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-introduction-memory-fences.aspx
источник
volatile
было необходимости на любой платформе , то это будет означать , что JIT не может оптимизировать загрузку памяти ,object s1 = syncRoot; object s2 = syncRoot;
чтобыobject s1 = syncRoot; object s2 = s1;
на этой платформе. Мне это кажется маловероятным.Есть способ реализовать это без
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; } } }
Понимание кода
Представьте, что внутри конструктора класса Singleton есть какой-то код инициализации. Если эти инструкции переупорядочиваются после того, как в поле установлен адрес нового объекта, то у вас есть неполный экземпляр ... представьте, что у класса есть этот код:
private int _value; public int Value { get { return this._value; } } private Singleton() { this._value = 1; }
Теперь представьте вызов конструктора с помощью оператора new:
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;
Есть ли разница? НЕТ, если вы думаете об одном потоке. ДА, если вы думаете о нескольких потоках ... что, если поток прерывается сразу после
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;
Удачного кодирования!
источник
new
полностью инициализирован ..Я не думаю, что кто-то действительно ответил на вопрос , поэтому я попробую.
Летучие и первые
if (instance == null)
не «нужны». Блокировка сделает этот код потокобезопасным.Возникает вопрос: зачем добавлять первое
if (instance == null)
?Причина, по-видимому, в том, чтобы избежать ненужного выполнения заблокированной части кода. Пока вы выполняете код внутри блокировки, любой другой поток, который пытается также выполнить этот код, блокируется, что замедлит вашу программу, если вы попытаетесь часто получить доступ к синглтону из многих потоков. В зависимости от языка / платформы могут возникнуть накладные расходы из-за самой блокировки, которых вы хотите избежать.
Таким образом, первая нулевая проверка добавляется как действительно быстрый способ узнать, нужна ли вам блокировка. Если вам не нужно создавать синглтон, вы можете полностью избежать блокировки.
Но вы не можете проверить, является ли ссылка нулевой, не заблокировав ее каким-либо образом, потому что из-за кэширования процессора другой поток может ее изменить, и вы прочитаете «устаревшее» значение, которое приведет к ненужному вводу блокировки. Но вы пытаетесь избежать блокировки!
Таким образом, вы делаете синглтон изменчивым, чтобы гарантировать, что вы читаете последнее значение, без необходимости использовать блокировку.
Вам по-прежнему нужна внутренняя блокировка, потому что volatile защищает вас только во время однократного доступа к переменной - вы не можете безопасно проверить и установить ее без использования блокировки.
Так вот, действительно ли это полезно?
Я бы сказал «в большинстве случаев нет».
Если Singleton.Instance может вызвать неэффективность из-за блокировок, то почему вы вызываете его так часто, что это может стать серьезной проблемой ? Весь смысл синглтона в том, что он только один, поэтому ваш код может прочитать и кэшировать ссылку на синглтон один раз.
Единственный случай, когда я могу думать о том, где это кеширование было бы невозможно, - это когда у вас есть большое количество потоков (например, сервер, использующий новый поток для обработки каждого запроса, может создавать миллионы очень коротких потоков, каждый из которых который должен был бы вызвать Singleton.Instance один раз).
Поэтому я подозреваю, что блокировка с двойной проверкой - это механизм, который имеет реальное место в очень специфических случаях, критичных к производительности, и затем все забрались на «это правильный способ сделать это», не задумываясь, что он делает и действительно ли будет действительно необходимо в случае, если они его используют.
источник
volatile
не имеет ничего общего с семантикой блокировки при блокировке с двойной проверкой, это имеет отношение к модели памяти и согласованности кеша. Его цель - гарантировать, что один поток не получит значение, которое все еще инициализируется другим потоком, что не предотвращает шаблон блокировки с двойной проверкой. В Java вам обязательно понадобитсяvolatile
ключевое слово; в .NET это непонятно, потому что это неправильно согласно ECMA, но верно согласно времени выполнения. В любом случае,lock
безусловно , никак не заботиться о нем.lock
делает код потокобезопасным. Это правда, но схема блокировки с двойной проверкой может сделать ее небезопасной . Это то, чего вам, кажется, не хватает. Этот ответ, кажется, беспокоит смысл и цель блокировки с двойной проверкой, не обращая внимания на проблемы безопасности потоков, которые являются причинойvolatile
.instance
он отмечен значкомvolatile
?Вы должны использовать volatile с шаблоном блокировки двойной проверки.
Большинство людей указывают на эту статью как на доказательство того, что вам не нужны volatile: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10
Но они не дочитают до конца: " Последнее слово предупреждения - я только предполагаю модель памяти x86 по наблюдаемому поведению на существующих процессорах. Таким образом, методы низкой блокировки также хрупки, потому что оборудование и компиляторы могут стать более агрессивными со временем. . Вот несколько стратегий, позволяющих минимизировать влияние этой хрупкости на ваш код. Во-первых, по возможности избегайте методов с низким уровнем блокировки. (...) Наконец, предположите, что модель памяти является самой слабой из возможных, используя изменчивые объявления вместо того, чтобы полагаться на неявные гарантии . "
Если вам нужно больше убедительности, прочтите эту статью о спецификации ECMA, которая будет использоваться для других платформ: msdn.microsoft.com/en-us/magazine/jj863136.aspx
Если вам нужно еще больше убедить, прочтите эту новую статью о том, что в нее могут быть внесены оптимизации, которые не позволят ей работать без volatile: msdn.microsoft.com/en-us/magazine/jj883956.aspx
Таким образом, на данный момент он «может» работать для вас без volatile, но не шансов, что он напишет правильный код и использует методы volatile или volatileread / write. В статьях, которые предлагают поступить иначе, иногда не учитываются некоторые возможные риски оптимизации JIT / компилятора, которые могут повлиять на ваш код, а также мы используем будущие оптимизации, которые могут произойти, что может привести к поломке вашего кода. Также, как упоминалось в предыдущей статье, предыдущие предположения о работе без volatile уже могут не работать на ARM.
источник
AFAIK (и - отнеситесь к этому с осторожностью, я не делаю много параллельных вещей) нет. Блокировка просто дает вам синхронизацию между несколькими соперниками (потоками).
volatile, с другой стороны, сообщает вашей машине, что нужно переоценивать значение каждый раз, чтобы вы не наткнулись на кешированное (и неправильное) значение.
См. Http://msdn.microsoft.com/en-us/library/ms998558.aspx и обратите внимание на следующую цитату:
Описание изменчивого: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx
источник
Думаю, я нашел то, что искал. Подробности в этой статье - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10 .
Подводя итог - в .NET модификатор volatile в этой ситуации действительно не нужен. Однако в более слабых моделях памяти записи, сделанные в конструкторе лениво инициированного объекта, могут быть отложены после записи в поле, поэтому другие потоки могут прочитать поврежденный ненулевой экземпляр в первом операторе if.
источник
lock
Достаточно. Сама спецификация языка MS (3.0) упоминает этот точный сценарий в §8.12 без какого-либо упоминанияvolatile
:источник
Это довольно хороший пост об использовании volatile с двойной проверкой блокировки:
http://tech.puredanger.com/2007/06/15/double-checked-locking/
В Java, если целью является защита переменной, вам не нужно блокировать, если она помечена как изменчивая.
источник