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

C# 4 в двух словах (настоятельно рекомендуется btw) использует следующий код для демонстрации концепции 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 будет иметь значение true.

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

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

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

2 ответа

Решение

Барьер № 2 гарантирует, что пишут _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 оказало влияние.

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

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

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

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

Ps. Эта статья хорошо объясняет внутреннюю работу x86.

Другие вопросы по тегам