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

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

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

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


person hackerhasid    schedule 16.08.2010    source источник
comment
Зачем кому-то использовать это вместо volatile?   -  person ChaosPandion    schedule 16.08.2010
comment
@Chaos: CLR через книгу C # (Richter) имеет отличное объяснение - IIRC это то, что «volatile» означает, что все обращения к var обрабатываются как volatile и обеспечивают полные барьеры памяти в обоих направлениях. Часто это намного больше производительности, чем необходимо, если вместо этого вам нужен только барьер чтения или записи и только в определенных случаях доступа.   -  person James Manning    schedule 16.08.2010
comment
@Chaos: не совсем в этом дело, но одна из причин заключается в том, что у volatile есть свои особенности в отношении оптимизации компилятора, которые могут привести к тупиковой ситуации, см. bluebytesoftware.com/blog/2009/02/24/   -  person hackerhasid    schedule 16.08.2010
comment
@statichippo: серьезно, если вы имеете дело с таким кодом (больше, чем просто изучаете его), пожалуйста, получите книгу Рихтера, я не могу ее порекомендовать. amazon.com/CLR-via-Dev-Pro-Jeffrey- Richter / dp / 0735627045   -  person James Manning    schedule 16.08.2010
comment
Я не на постоянной основе, но книгу обязательно получу!   -  person hackerhasid    schedule 16.08.2010
comment
@James - Имеет смысл, в основном я экспериментировал с неизменяемостью с некоторой блокировкой света, когда возникает необходимость. Можете ли вы привести небольшой пример, где это было бы полезно?   -  person ChaosPandion    schedule 16.08.2010
comment
@James: ключевое слово volatile устанавливает половинные барьеры (загрузка-получение + сохранение-выпуск), а не полные барьеры. Если вы цитируете Рихтера, то он ошибается в этом вопросе. Хорошее объяснение есть в книге Джо Даффи «Параллельное программирование в Windows».   -  person Joe Albahari    schedule 17.08.2010
comment
@albahari - Я уверен, что я неправильно запомнил этот отрывок из книги Рихтера - по общему признанию, это то, с чем мне не приходилось сталкиваться в моем собственном кодировании.   -  person James Manning    schedule 17.08.2010
comment
Я начинаю задаваться вопросом, писал ли кто-нибудь когда-нибудь кусок кода, который требовал MemoryBarriers, в котором не было ошибки.   -  person Martin Brown    schedule 03.11.2011
comment
Что касается изменчивости, ее лучше всего понять, если вы думаете об этом как об обеспечении no барьеров памяти, но вместо этого предотвращая оптимизацию компилятора и процессора, которая может вызвать неожиданное поведение из-за подобного подъема переменных. играть с огнем.   -  person IamIC    schedule 27.02.2017
comment
Эта тема почти нигде не упоминается, я обнаружил это чисто случайным образом. Интересно, кто наклеивает свой код барьерами, как показано, кто понимает все это достаточно, чтобы предотвратить каждый случай? Кто может эффективно читать этот код? Могу поспорить, что большинство приложений полностью не защищены. Вау ... это страшно.   -  person Droidum    schedule 14.03.2018


Ответы (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 оказало влияние.

person Brian Gideon    schedule 16.08.2010
comment
Спасибо, это было полезно. Думаю, я был не так уж глуп, как думал. - person hackerhasid; 16.08.2010
comment
Я не понимаю, что оба барьера 2 и 3 необходимы в случае, если B бежит за A. Оба являются полными заборами, поэтому любой из них будет действовать в одиночку, не так ли? - person Ohad Schneider; 05.07.2011
comment
@ohadsc: барьеры памяти влияют на поведение только одного потока. Учтите, что A и B могут работать на разных процессорах. Если вы удалили барьер 2, запись может не выполняться. Если вы удалили барьер 3, то считывание может не обновиться. Барьеры в A не влияют на выполнение B и наоборот. - person Brian Gideon; 05.07.2011
comment
Спасибо, теперь я понимаю. Если у вас есть время, посмотрите мой вопрос относительно вашего ответа здесь: stackoverflow.com/questions/6574389/ - person Ohad Schneider; 05.07.2011
comment
Я не понимаю барьер памяти №4 (он нужен?). # 3 уже гарантирует, что мы аннулируем кеш памяти и имеем актуальные значения. И _answer гарантированно будет иметь значение первым. Что мне не хватает? - person Erti-Chris Eelmaa; 16.04.2013
comment
@ Erti-ChrisEelmaa: Барьер № 4 предотвращает чтение _answer до _complete, что может привести к тому, что программа напечатает 0, если A и B чередуются. - person Brian Gideon; 08.09.2013

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

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

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

person IamIC    schedule 27.02.2017