Вопрос по concurrency, c# – Синглтон дважды проверяет проблему параллелизма

3

Следующее предложение взято с jetbrains.net Прочитав эту и некоторые другие статьи в Интернете, я все еще не понимаю, как можно вернуть значение null после того, как первый поток войдет в блокировку. Кто-то, кто понимает это, может помочь мне и объяснить это более гуманным способом?

& quot; рассмотрим следующий фрагмент кода:

public class Foo
{
  private static Foo instance;
  private static readonly object padlock = new object();

  public static Foo Get()
  {
    if (instance == null)
    {
      lock (padlock)
      {
        if (instance == null)
        {
          instance = new Foo();
        }
      }
    }
    return instance;
  }
};

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

Чтобы избежать этого, значение экземпляра должно быть изменчивым. & Quot;

Увидетьcsharpindepth.com/Articles/General/Singleton.aspx SLaks
Связанные с:stackoverflow.com/questions/8358707/…. Steven

Ваш Ответ

2   ответа
12

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

class Foo
{
  public int variable1;
  public int variable2;

  public Foo()
  {
    variable1 = 1;
    variable2 = 2;
  }
}

Вот как код может быть оптимизирован компилятором C #, JIT или аппаратным обеспечением.1

if (instance == null)
{
  lock (padlock)
  {
    if (instance == null)
    {
      instance = alloc Foo;
      instance.variable1 = 1; // inlined ctor
      instance.variable2 = 2; // inlined ctor
    }
  }
}
return instance;

Во-первых, обратите внимание, что конструктор встроен (потому что это было просто). Теперь, надеюсь, легко увидеть, чтоinstance получает ссылку до того, как его составляющие поля инициализируются внутри конструктора. Это правильная стратегия, потому что чтение и запись могут свободно перемещаться вверх и вниз, если они не пересекают границыlock или изменитьlogical течь; что они не делают. Так что другой поток мог видетьinstance != null и попытайтесь использовать его до полной инициализации.

volatile решает эту проблему, потому что он рассматривает чтение какacquire fence и пишет какrelease fence.

  • acquire-fence: A memory barrier in which other reads & writes are not allowed to move before the fence.
  • release-fence: A memory barrier in which other reads & writes are not allowed to move after the fence.

Так что, если мы отметимinstance какvolatile тогда деблокирующий забор предотвратит вышеуказанную оптимизацию. Вот как будет выглядеть код с аннотациями барьера. Я использовал & # x2191; стрелка для обозначения спускового ограждения и символа & # x2193; стрелка для обозначения ограждения. Обратите внимание, что ничто не может проплыть мимо & # x2191; стрелка или вверх мимо & # x2193; стрела. Думайте о наконечнике стрелы как о отталкивающем все

var local = instance;
↓ // volatile read barrier
if (local == null)
{
  var lockread = padlock;
  ↑ // lock full barrier
  lock (lockread)
  ↓ // lock full barrier
  {
    local = instance;
    ↓ // volatile read barrier
    if (local == null)
    {
      var ref = alloc Foo;
      ref.variable1 = 1; // inlined ctor
      ref.variable2 = 2; // inlined ctor
      ↑ // volatile write barrier
      instance = ref;
    }
  ↑ // lock full barrier
  }
  ↓ // lock full barrier
}
local = instance;
↓ // volatile read barrier
return local;

Записи в составляющие переменныеFoo все еще может быть переупорядочен, но обратите внимание, что барьер памяти теперь предотвращает их возникновение после назначенияinstance, Используя стрелки в качестве руководства, представьте различные стратегии оптимизации, которые разрешены и запрещены. Помните, что не читаетor записи разрешены плавать мимо & # x2191; стрелка или вверх мимо & # x2193; стрела.

Thread.VolatileWrite решил бы и эту проблему и мог бы использоваться на языках безvolatile Ключевое слово, как VB.NET. Если вы посмотрите на то, какVolatileWrite реализован, вы бы увидели это.

public static void VolatileWrite(ref object address, object value)
{
  Thread.MemoryBarrier();
  address = value;
}

Поначалу это может показаться нелогичным. В конце концов, барьер памяти установленbefore назначение. Как насчет того, чтобы получить задание, зафиксированное в основной памяти? Не будет ли правильнее поставить барьерafter назначение? Если это то, что ваша интуиция говорит вам, то этоwrong, Вы видите, что барьеры памяти не сводятся к получению «свежего чтения» или "совершенная запись". Все дело в заказе команд. Это самый большой источник путаницы, который я вижу.

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

public static void VolatileWrite(ref object address, object value)
{
  ↑ // full barrier
  ↓ // full barrier
  address = value;
}

Так технически зоветVolatileWrite делает больше, чем то, что пишет вvolatile поле будет делать. Помни чтоvolatile не допускается в VB.NET, например, ноVolatileWrite является частью BCL, поэтому его можно использовать на других языках.


1This optimization is mostly theoretical. The ECMA specification does technically allow for it, but the Microsoft CLI implementation of the ECMA specification treats all writes as if they had release fence semantics already. It is possible that another implementation of the CLI could still perform this optimization though.

+1 очень хороший ответ
@ Steven: Да, добавил заметку. Более того, x86-процессоры уже делают записи-релизы. Я также упомянул это в своем ответе на вопрос, который вы цитировали.
Спасибо @BrianGideon за объяснение. Если второй поток ожидает снятия блокировки, возможно, что когда он входит в заблокированную область, он все еще видит экземпляр как нулевой? Другими словами, освобождение и приобретение замка гарантируют видимость? MemoryBarrier, как объяснено, только налагает ограничения на переупорядочение команд, но не гарантирует «свежесть».
То, что вы описываете, никогда не произойдет с моделью памяти CLR 2.0. Модель памяти CLR изменилась, потому что это было проблемой в 1.0 и 1.1, и другие платформы, такие как Mono, могли также иметь эту проблему. Другими словами, двойная проверка блокировки хороша для .NET 2.0 и выше. Посмотреть здесь:stackoverflow.com/questions/8358707/…
3

Билл Пью написал несколько статей на эту тему, и является ссылкой на тему.

Заметная ссылка& Quot; Двойная проверка блокировки заблокирована & quot; Декларация.

Грубо говоря, вот в чем проблема:

В многоядерной виртуальной машине записи одним потоком могут быть невидимы для другого потока, пока не будет достигнут барьер синхронизации (или ограничения памяти). Вы можете прочитать & quot;Барьеры памяти: аппаратное представление для хакеров программного обеспечения& Quot; это действительно хорошая статья по этому вопросу.

Итак, если поток инициализирует объектA с одним полемaи сохраняет ссылку на объект в полеref другого объектаBу нас есть две «ячейки» в памяти:a, а такжеref, Изменения в обеих областях памяти могут не стать видимыми для других потоков одновременно, если только эти потоки не обеспечивают видимость изменений с помощью ограничителя памяти.

В Java синхронизацию можно принудительноsynchronized, Это дорого, и альтернатива объявлению поля какvolatile в этом случае изменение этой ячейки всегда видно всем потокам.

НО, семантика изменчивых изменений между Java 4 и 5. В Java 4 вам нужно определить обаa, а такжеref как изменчивый, для проверки doulbe, чтобы работать в примере, который я описал.

Это не было интуитивно понятно, и большинство людейref как изменчивый. Таким образом, они изменяют это и в Java 5+, если изменяемое поле изменено (ref) запускает синхронизацию других измененных полей (a).

РЕДАКТИРОВАТЬ: я вижу только сейчас, когда вы спрашиваете C #, а не Java ... Я оставляю свой ответ, потому что, возможно, это все-таки полезно.

Похожие вопросы