Зачем мне нужен барьер памяти?
С# 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?
Ответы
Ответ 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.