В первом случае Барьер 1 гарантирует, что _answer
написано ДО _complete
. Независимо от того, как написан код или как компилятор или CLR инструктирует ЦП, очереди чтения / записи на шине памяти могут переупорядочивать запросы. Барьер в основном говорит «очистить очередь, прежде чем продолжить». Аналогично, Барьер 4 гарантирует, что _answer
прочитано ПОСЛЕ _complete
. В противном случае CPU2 может изменить порядок вещей и увидеть старый _answer
с «новым» _complete
.
Барьеры 2 и 3 в некотором смысле бесполезны. Обратите внимание, что в объяснении содержится слово «после»: то есть «... если B побежал за A, ...». Что значит для Б бежать за А? Если B и A находятся на одном и том же процессоре, то, конечно, B может быть после. Но в этом случае тот же процессор означает отсутствие проблем с памятью.
Итак, рассмотрим B и A, работающие на разных процессорах. Теперь, во многом как теория относительности Эйнштейна, концепция сравнения времени в разных местах / процессорах на самом деле не имеет смысла.
Другой способ думать об этом - можете ли вы написать код, который может сказать, бежал ли B после A? Если так, то вы, вероятно, использовали для этого барьеры памяти. В противном случае, вы не можете сказать, и нет смысла спрашивать. Это также похоже на принцип Гейзенбурга - если вы можете наблюдать его, вы изменили эксперимент.
Но оставив физику в стороне, допустим, вы могли бы открыть капот своей машины, и увидеть , что на самом деле ячейка памяти _complete
была истинной (потому что А работал). Теперь запустите B. без Барьера 3, CPU2 может ЕЩЕ НЕ видеть _complete
как истину. т.е. не "свежий".
Но вы, вероятно, не можете открыть свою машину и посмотреть на _complete
. Не сообщайте свои выводы B на CPU2. Ваша единственная связь - это то, что делают сами процессоры. Так что, если они не могут определить BEFORE / AFTER без барьеров, спрашивать «что будет с B, если он работает после A, без барьеров» не имеет смысла .
Между прочим, я не уверен, что у вас есть в C #, но что обычно делается, и что действительно нужно для примера кода № 1 - это барьер с одним выпуском при записи и один барьер при чтении при чтении :
void A()
{
_answer = 123;
WriteWithReleaseBarrier(_complete, true); // "publish" values
}
void B()
{
if (ReadWithAcquire(_complete)) // subscribe
{
Console.WriteLine (_answer);
}
}
Слово «подписаться» не часто используется для описания ситуации, но «опубликовать» есть. Предлагаю вам прочитать статьи Херба Саттера о потоках.
Это ставит барьеры в точно правильных местах.
Для примера кода № 2 это не проблема барьера памяти, это проблема оптимизации компилятора - она хранит complete
в регистре. Барьер памяти вытеснит его, как и volatile
, но, вероятно, вызовет внешнюю функцию - если компилятор не может определить, изменила ли эта внешняя функция complete
или нет, он перечитает ее из памяти. то есть возможно передать адрес complete
какой-либо функции (определенной где-то, где компилятор не может проверить ее детали):
while (!complete)
{
some_external_function(&complete);
}
, даже если функция не изменяет complete
, если компилятор не уверен, ему потребуется перезагрузить свои регистры.
то есть разница между кодом 1 и кодом 2 заключается в том, что код 1 имеет проблемы только тогда, когда A и B работают в отдельных потоках. Код 2 может иметь проблемы даже на однопоточном компьютере.
На самом деле, другой вопрос - может ли компилятор полностью удалить цикл while? Если он считает, что complete
недоступен другим кодом, почему бы и нет? то есть, если он решил переместить complete
в регистр, он мог бы также полностью удалить цикл.
РЕДАКТИРОВАТЬ: Чтобы ответить на комментарий от opc (мой ответ слишком велик для блока комментариев):
Барьер 3 заставляет ЦП сбрасывать все ожидающие запросы на чтение (и запись).
Итак, представьте, были ли какие-то другие чтения перед чтением _complete:
void B {}
{
int x = a * b + c * d; // read a,b,c,d
Thread.MemoryBarrier(); // Barrier 3
if (_complete)
...
Без барьера ЦП может иметь все эти 5 запросов на чтение «в ожидании»:
a,b,c,d,_complete
Без барьера процессор может переупорядочить эти запросы для оптимизации доступа к памяти (т. Е. Если _complete и 'a' находятся в одной строке кэша или что-то в этом роде).
При наличии барьера ЦП получает a, b, c, d обратно из памяти, ДО того, как _complete будет вставлен как запрос. ОБЕСПЕЧЕНИЕ 'b' (например) читается ДО _complete - т.е. без переупорядочения.
Вопрос - какая разница?
Если a, b, c, d не зависят от _complete, то это не имеет значения. Все, что делает барьер - это МЕДЛЕННЫЕ ВНИЗ. Так что да, _complete
читается позже . Таким образом, данные свежее . Помещение цикла sleep (100) или некоторого занятого ожидания для ожидания, прежде чем чтение также сделает его «более свежим»! : -)
Так что суть в том - держи это относительно. Нужно ли читать / записывать данные ДО / ПОСЛЕ относительно других данных или нет? Вот в чем вопрос.
И чтобы не опускать автора статьи - он упоминает "если Б побежал за А ...". Просто не совсем ясно, представляет ли он, что B после A имеет решающее значение для кода, является видимым для кода или просто несущественным.