Я пытаюсь обдумать тонкости, связанные с барьерами памяти и изменчивыми операциями чтения / записи. Я читаю здесь статью Джозефа Альбахари:
http://www.albahari.com/threading/part4.aspx
И спотыкаясь о том, когда нужен барьер памяти перед чтением / записью, а когда он нужен после. В этом разделе кода в разделе «полная ограда» он ставит барьер памяти после каждой записи и перед каждым чтением:
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: У меня нет проблем с барьерами 1 и 4, поскольку это предотвратит изменение порядка через эти барьеры. Но я не совсем понимаю, зачем нужны барьеры 2 и 3. Может кто-нибудь объяснить, особенно с учетом того, как изменчивые операции чтения и записи реализованы в классе Thread
(поясняется далее)?
Теперь я действительно начинаю путаться в том, что это фактическая реализация Thread.VolatileRead/Write()
:
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void VolatileWrite (ref int address, int value)
{
MemoryBarrier(); address = value;
}
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static int VolatileRead (ref int address)
{
int num = address; MemoryBarrier(); return num;
}
Как видите, в отличие от предыдущего примера, встроенные энергозависимые функции устанавливают барьеры памяти перед каждой записью (а не после) и после каждого чтения (а не до). Поэтому, если бы мы переписали предыдущий пример с эквивалентной версией, основанной на встроенных изменчивых функциях, вместо этого это выглядело бы так:
class Foo
{
int _answer;
bool _complete;
void A()
{
Thread.MemoryBarrier(); // Barrier 1
_answer = 123;
Thread.MemoryBarrier(); // Barrier 2
_complete = true;
}
void B()
{
if (_complete)
{
Thread.MemoryBarrier(); // Barrier 3
Console.WriteLine (_answer);
Thread.MemoryBarrier(); // Barrier 4
}
}
}
Вопрос № 2: Являются ли оба класса Foo функционально эквивалентными? Почему или почему нет? Если барьеры 2 и 3 (в первом классе Foo) необходимы для гарантии записи значения и чтения фактического значения, тогда не будут ли методы Thread.VolatileXXX
бесполезными?
В StackOverflow есть пара похожих вопросов с принятыми ответами, например «барьер 2 гарантирует, что запись в _complete не кэшируется», но ни один из них не касается того, почему Thread.VolatileWrite()
ставит барьер памяти перед записью, если это так, и почему барьер 3 является необходимо, если Thread.VolatileRead()
устанавливает барьер памяти после чтения, но гарантирует актуальность значения. Я думаю, это то, что меня больше всего сбивает с толку.
ОБНОВЛЕНИЕ:
Итак, после дополнительного чтения и размышлений у меня есть теория, и я обновил исходный код атрибутами, которые, по моему мнению, могут быть актуальными. Я не думаю, что барьеры памяти в Thread.VolatileRead/Write
методах существуют вообще для обеспечения «свежести» значений, а скорее для обеспечения гарантий переупорядочения. Установка барьера после чтения и перед записью гарантирует, что никакие записи не будут перемещены до любых операций чтения (но не наоборот).
Из того, что мне удалось найти, все операции записи на x86 гарантируют согласованность кеша за счет аннулирования их строки кэша на других ядрах, поэтому "свежесть" гарантирована, пока значение не кэшируется в регистре. Моя теория на пути VolatileRead/Write
обеспечения того, чтобы значение не было в регистре, что может быть далеко, но я думаю, что я на правильном пути, заключается в том, что они рассчитывают на детали реализации .NET, где если они помечены MethodImplOptions.NoInlining
(что, как вы можете видеть выше), тогда значение нужно будет передать в / из метода вместо того, чтобы быть встроенным как локальная переменная, и, следовательно, к нему нужно будет получить доступ из памяти / кеша, а не напрямую через регистр, что устраняет необходимость в дополнительном барьере памяти после записи и перед чтением. Понятия не имею, так ли это, но это единственный способ увидеть, как он работает правильно.
Может ли кто-нибудь подтвердить или опровергнуть, что это так?