Зачем мне барьер памяти?
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 оказало влияние.
Пример неясен по двум причинам:
- Это слишком просто, чтобы полностью показать, что происходит с заборами.
- Albahari включает требования для архитектур не x86. См. MSDN: "MemoryBarrier требуется только в многопроцессорных системах со слабым упорядочением памяти (например, в системе, использующей несколько процессоров Intel Itanium [которые Microsoft больше не поддерживает])".
Если учесть следующее, становится понятнее:
- Барьер памяти (здесь полные барьеры - .Net не обеспечивает половину барьера) не позволяет инструкциям чтения / записи пересекать границы (из-за различных оптимизаций). Это гарантирует нам, что код после забора будет выполняться после кода до забора.
- "Эта операция сериализации гарантирует, что каждая инструкция загрузки и сохранения, которая предшествует в программном порядке команде MFENCE, будет видна глобально, прежде чем любая команда загрузки или сохранения, которая следует за инструкцией MFENCE, станет видимой глобально". Смотрите здесь.
- Процессоры x86 имеют сильную модель памяти, и гарантийные записи выглядят согласованными для всех потоков / ядер (поэтому барьеры № 2 и № 3 на x86 не нужны). Но мы не гарантируем, что чтение и запись останутся в кодированной последовательности, следовательно, необходимы барьеры № 1 и № 4.
- Барьеры памяти неэффективны и не должны использоваться (см. Ту же статью MSDN). Лично я использую Interlocked и volatile (убедитесь, что вы знаете, как правильно его использовать!!), которые работают эффективно и просты для понимания.
Ps. Эта статья хорошо объясняет внутреннюю работу x86.