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

Ответ 1

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

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

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

И не забудьте проверить Jon Skeet руководство по реализации singleton в С#


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

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

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

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

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

Серия 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

Ответ 2

Существует способ реализовать его без поля 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;
}

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

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;

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

Ответ 3

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

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

Итак, встает вопрос: зачем вы добавляете первый if (instance == null)?

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

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

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

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

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

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

Хорошо, я бы сказал "в большинстве случаев, нет".

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

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

Таким образом, я подозреваю, что двойная проверенная блокировка - это механизм, который имеет реальное место в очень специфических случаях с критичной критикой, а затем все взобрались на "это правильный способ сделать это", победив, фактически не подумав о том, что он делает и действительно ли это будет необходимо в том случае, если они используют его для.

Ответ 4

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

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

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

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

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

Ответ 5

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

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

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

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

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

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

Ответ 6

Достаточно 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) {
          ...
        }
    }
}

Ответ 7

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

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

Ответ 8

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

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

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