Подтвердить что ты не робот

Зачем мне нужен барьер памяти?

С# 4 в двух словах (настоятельно рекомендуется кстати) использует следующий код, чтобы продемонстрировать концепцию MemoryBarrier (предполагая, что A и B были запущены на разных потоках):

class Foo{
  int _answer;
  bool complete;
  void A(){
    _answer = 123;
    Thread.MemoryBarrier(); // Barrier 1
    _complete = true;
    Thread.MemoryBarrier(); // Barrier 2
  }
  void B(){
    Thread.MemoryBarrier(); // Barrier 3;
    if(_complete){
      Thread.MemoryBarrier(); // Barrier 4;
      Console.WriteLine(_answer);
    }
  }
}

они упоминают, что барьеры 1 и 4 не позволяют этому примеру писать 0, а барьеры 2 и 3 обеспечивают гарантию свежести: они гарантируют, что если B пробежал после A, чтение _complete будет оцениваться как правда.

Я не понимаю. Я думаю, что я понимаю, почему нужны барьеры 1 и 4: мы не хотим, чтобы запись _answer была оптимизирована и помещена после записи в _complete (Barrier 1) и мы должны убедиться, что _answer не кэшируется (Barrier 4). Я также думаю, что я понимаю, почему Барьер 3 необходим: если A пробежал до тех пор, пока не напишет _complete = true, B все равно нужно обновить _complete, чтобы прочитать правильное значение.

Я не понимаю, почему нам нужен барьер 2! Часть меня говорит, что это потому, что Thread 2 (работа B) уже выполнялся до (но не включая) if (_complete), и поэтому нам нужно убедиться, что _complete обновлен,

Однако я не вижу, как это помогает. Разве не возможно, что _complete будет установлено значение true в A, но все же метод B увидит кэшированную (ложную) версию _complete? То есть, если Thread 2 запускал метод B до первого MemoryBarrier, а затем Thread 1 запускал метод A до _complete = true, но не более того, а затем Thread 1 возобновил и протестировал if (_complete) - может ли , если не соответствовать false?

4b9b3361

Ответ 1

Barrier # 2 guarentees, что запись в _complete происходит немедленно. В противном случае он может оставаться в состоянии очереди, означающем, что чтение _complete в B не увидит изменения, вызванные A, хотя B эффективно использовал изменчивое чтение.

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

Ответ на ваш вопрос о том, может ли if по-прежнему оценивать значение false, да, именно по причинам, указанным вами. Но заметьте, что автор говорит об этом.

Барьеры 1 и 4 предотвращают этот пример от записи "0". Барьеры 2 и 3 предоставить гарантию свежести: они убедитесь, что , если B пробежал после A, показав _complete будет иметь значение true.

Акцент на том, "если Б побежал за А", принадлежит мне. Конечно, это может быть случай, когда два потока чередуются. Но автор игнорировал этот сценарий, предположительно, чтобы высказать свою мысль о том, как Thread.MemoryBarrier работает проще.

Кстати, мне нелегко было придумать пример на моей машине, где барьеры №1 и №2 изменили бы поведение программы. Это связано с тем, что модель памяти в отношении записей была сильной в моей среде. Возможно, если бы у меня была многопроцессорная машина, я использовал Mono или имел другую другую настройку, которую я мог бы продемонстрировать. Конечно, было легко продемонстрировать, что устранение барьеров № 3 и № 4 оказало влияние.

Ответ 2

Пример неясен по двум причинам:

  • Слишком просто полностью показать, что происходит с забором.
  • Albahari включает требования для архитектур без архитектуры x86. См. MSDN: "MemoryBarrier требуется только в многопроцессорных системах со слабым порядком памяти (например, в системе, использующей несколько процессоров Intel Itanium [которые Microsoft больше не поддерживает]).".

Если вы считаете следующее, оно становится понятным:

  • Барьер памяти (полный барьер здесь -.Net не обеспечивает полубарьер) не позволяет инструкциям чтения/записи перескакивать забор (из-за различных оптимизаций). Это гарантирует нам код после того, как забор будет выполнен после кода перед забором.
  • "Эта операция сериализации гарантирует, что каждая инструкция по загрузке и хранению, которая предшествует порядку программы, команда MFENCE отображается глобально, прежде чем любая инструкция по загрузке или хранению, следующая за инструкцией MFENCE, будет видна на глобальном уровне." См. здесь.
  • x86 Процессоры имеют сильную модель памяти, и гарантированные записи кажутся согласованными со всеми нитями/ядрами (поэтому на #86 не требуются барьеры # 2 и # 3). Но нам не гарантировано, что чтение и запись будут оставаться в кодированной последовательности, следовательно, необходимы барьеры № 1 и № 4.
  • Пункты памяти неэффективны и их не нужно использовать (см. ту же статью MSDN). Я лично использую Interlocked и volatile (убедитесь, что вы знаете, как правильно его использовать!!), которые работают эффективно и понятны.

Ps. В этой статье хорошо объясняется внутренняя работа x86.