это зависит от того, что означает «свежий». Thread.MemoryBarrier
заставит первое считывание переменной получить, загрузив ее из ее назначенной ячейки памяти. Если это все, что вы имеете в виду под «свежим» и не более чем, то ответ «да». Большинство программистов работают с более жестким определением, понимают ли они это или нет, и именно там начинаются проблемы и путаница. Обратите внимание, что волатильное чтение через volatile
и другие аналогичные механизмы не производят «свежие» данные под этим определением, но под другим определением. Продолжайте читать, чтобы узнать, как это сделать.
Я буду использовать стрелку вниз ↓ для представления изменчивого считывания и стрелки вверх ↑ для представления волатильной записи. Подумайте о том, что голова стрелки отталкивает другие чтения и записи. Код, который генерирует эти заграждения памяти, может свободно перемещаться, пока никакая команда не продвигается по стрелке вниз и вниз по стрелке вверх. Запоминания памяти (стрелки), однако, заблокированы на месте в месте, где они были первоначально объявлены в коде. Thread.MemoryBarrier
создает барьер с полным заграждением, поэтому он имеет как семантику считывания, так и освобождение-запись.
int a = 0;
int b = 0;
void A() // runs in thread A
{
register = 1
a = register
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
register = b
jump Console.WriteLine
use register
return Console.WriteLine
}
void B() // runs in thread B
{
register = 1
b = register
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
register = a
jump Console.WriteLine
use register
return Console.WriteLine
}
Имейте в виду, что линии C# на самом деле являются множественными инструкциями после их компиляции и выполнения JIT. Я попытался проиллюстрировать это несколько, но на самом деле вызов Console.WriteLine
по-прежнему будет намного сложнее, чем показано, поэтому время между чтением a
или b
и их первое использование может быть значительным относительно. Поскольку Thread.MemoryBarrier
создает забор, считываниям не разрешается всплывать и проходить вызов. Таким образом, чтение является «свежим» относительно вызова Thread.MemoryBarrier
. Но он может быть «устаревшим» относительно того, когда он фактически используется вызовом Console.WriteLine
.
Давайте рассмотрим, как выглядит ваш код, если мы заменим вызов Thread.MemoryBarrier
ключевым словом volatile
.
volatile int a = 0;
volatile int b = 0;
void A() // runs in thread A
{
register = 1
↑ // volatile write
a = register
register = b
↓ // volatile read
jump Console.WriteLine
use register
return Console.WriteLine
}
void B() // runs in thread B
{
register = 1
↑ // volatile write
b = register
register = a
↓ // volatile read
jump Console.WriteLine
use register
return Console.WriteLine
}
Можете ли вы определить изменение? Если вы моргнули, вы пропустили его. Сравните расположение стрелок (заграждений памяти) между двумя блоками кода. В первом случае (Thread.MemoryBarrier
) считывание не допускается в момент времени до барьера памяти. Но во втором случае (volatile
) чтение может пузыриться бесконечно (потому что есть стрелка вниз, отталкивающая их). В этом случае можно сделать разумный аргумент, согласно которому Thread.MemoryBarrier
может производить «свежее» чтение, если оно помещено перед чтением, чем решение volatile
. Но, можете ли вы по-прежнему утверждать, что чтение «свежее»? Не совсем потому, что к моменту его использования Console.WriteLine
это может быть не последнее значение.
Так что же вы можете задать volatile
. Поскольку последовательные чтения производят семантику получения-забора, она гарантирует, что последующие чтения производят более новое значение, чем предыдущее чтение. Рассмотрим следующий код.
volatile int a = 0;
void A()
{
register = a;
↓ // volatile read
Console.WriteLine(register);
register = a;
↓ // volatile read
Console.WriteLine(register);
register = a;
↓ // volatile read
Console.WriteLine(register);
}
Обратите внимание на то, что может случиться здесь. Показаны строки register = a
. Обратите внимание, где находится стрелка ↓. Поскольку он помещается после чтения, нет ничего, что предотвращало бы плавание вверх. Он может фактически всплывать и до предыдущего вызова Console.WriteLine
. Таким образом, в этом случае нет гарантии, что Console.WriteLine
работает с последним значением a
. Тем не менее, гарантируется, что он будет работать с более новым значением, чем в последний раз, когда он был вызван. Это его полезность в двух словах. Вот почему вы видите много незакрепленного кода, вращающегося в цикле while, чтобы предыдущее чтение изменчивой переменной равно текущему чтению, прежде чем предполагать, что его предполагаемая операция прошла успешно.
Есть несколько важных моментов, которые я хочу сделать в заключение.
Thread.MemoryBarrier
гарантирует, что чтение появляется после того, как он возвращает последнее значение по отношению к барьеру. Но к тому времени, когда вы на самом деле принимаете решения или используете эту информацию, она больше не может быть последней ценностью.
volatile
гарантирует, что чтение вернет значение, которое является новее, чем предыдущее чтение той же переменной. Ни в коем случае это не гарантирует, что значение является последним, хотя.
- Значение «свежего» должно быть четко определено, но может отличаться от ситуации к ситуации и разработчика для разработчика. Нет никакого смысла, который будет более правильным, чем что-либо другое, если оно может быть формально определено и сформулировано.
- Это не абсолютная концепция. Вы найдете более полезным определить «свежие» с точки зрения того, чтобы быть относительно чего-то другого, например, создания барьера памяти или предыдущей инструкции. Другими словами, «свежесть» - это относительное понятие, как скорости относительно наблюдателя в теории специальной теории относительности Эйнштейна.
Возможный дубликат [Что такое забор памяти? ] (http://stackoverflow.com/questions/286629/what-is-a-memory-fence) – nothrow
@Yossarian это не дубликат по причинам, которые я объясняю. – Petrakeas