Где разместить ограждения / барьеры памяти, чтобы гарантировать новое чтение / совершенные записи?
Как и многие другие люди, меня всегда смущали изменчивые чтения / записи и заборы. Так что теперь я пытаюсь полностью понять, что они делают.
Итак, предполагается, что энергозависимое чтение (1) демонстрирует семантику захвата и (2) гарантирует, что считанное значение является новым, т. Е. Оно не является кэшированным значением. Давайте сосредоточимся на (2).
Итак, я прочитал, что, если вы хотите выполнить энергозависимое чтение, вы должны ввести забор чтения (или полный забор) после чтения, например:
int local = shared;
Thread.MemoryBarrier();
Как именно это предотвращает использование операции чтения ранее кэшированного значения? В соответствии с определением ограждения (запрещается чтение / хранение над / под ограждением), я вставляю ограждение перед чтением, чтобы чтение не пересекало ограждение и не перемещалось назад во времени (иначе кэшируется).
Каким образом предотвращение перемещения чтения во времени вперед (или последующие инструкции не могут быть перемещены назад во времени) гарантирует изменчивое (новое) чтение? Как это помогает?
Точно так же я считаю, что энергозависимая запись должна вводить забор после операции записи, не позволяя процессору перемещать запись вперед во времени (иначе говоря, задерживая запись). Я считаю, что это заставит процессор сбрасывать записи в основную память.
Но, к моему удивлению, реализация C# вводит забор перед записью!
[MethodImplAttribute(MethodImplOptions.NoInlining)] // disable optimizations
public static void VolatileWrite(ref int address, int value)
{
MemoryBarrier(); // Call MemoryBarrier to ensure the proper semantic in a portable way.
address = value;
}
Обновить
В соответствии с этим примером, по-видимому, взятым из "C# 4 в двух словах", забор 2, помещенный после записи, должен принудительно немедленно перезаписать запись в основную память, а забор 3, помещенный перед чтением, должен гарантировать свежее чтение:
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);
}
}
}
Идеи в этой книге (и мои личные убеждения), похоже, противоречат идеям, стоящим за C#. VolatileRead
а также VolatileWrite
Реализации.
2 ответа
Как именно это предотвращает использование операции чтения ранее кэшированного значения?
Это не такая вещь. Изменчивое чтение не гарантирует, что будет возвращено последнее значение. На простом английском языке все это действительно означает, что следующее чтение вернет более новое значение и ничего более.
Каким образом предотвращение перемещения чтения во времени вперед (или последующие инструкции не могут быть перемещены назад во времени) гарантирует изменчивое (новое) чтение? Как это помогает?
Будьте осторожны с терминологией здесь. Летучий не является синонимом свежего. Как я уже упоминал выше, его реальная полезность заключается в том, как два или более изменчивых чтения связаны друг с другом. Следующее чтение в последовательности изменчивых чтений будет абсолютно возвращать более новое значение, чем предыдущее чтение по тому же адресу. Код без блокировки должен быть написан с учетом этой предпосылки. То есть код должен быть структурирован так, чтобы работать по принципу работы с более новым значением, а не с последним значением. Вот почему большая часть кода без блокировки вращается в цикле, пока не сможет убедиться, что операция прошла полностью успешно.
Идеи в этой книге (и мои личные убеждения), кажется, противоречат идеям, лежащим в основе реализаций C# VolatileRead и VolatileWrite.
На самом деле, нет. Помните изменчивый!= Свежий. Да, если вы хотите "свежее" чтение, тогда вам необходимо установить ограждение перед чтением. Но это не то же самое, что делать волатильное чтение. Я говорю, что если реализация VolatileRead
был вызов Thread.MemoryBarrier
перед инструкцией чтения, то это фактически не произвело бы непостоянное чтение. Если бы выпустил свежие читать, хотя.
Важно понимать, что volatile
не только означает "не может кэшировать значение", но также дает важные гарантии видимости (точнее, вполне возможно иметь энергозависимую запись, которая идет только в кэш; зависит исключительно от аппаратного обеспечения и его используемых протоколов когерентности кэша)
Энергозависимое чтение дает семантику получения, в то время как энергозависимая запись имеет семантику выпуска. Забор "забор" означает, что вы не можете изменить порядок чтения или записи перед забором, а "забор" означает, что вы не можете переместить их за забор. Связанный ответ в комментариях объясняет это на самом деле довольно хорошо.
Теперь вопрос в том, если у нас не будет никакого барьера памяти перед загрузкой, как гарантируется, что мы увидим самое новое значение? Ответ таков: потому что мы также ставим барьеры памяти после каждой изменчивой записи, чтобы гарантировать это.
Даг Ли написал отличную сводку о том, какие существуют барьеры, что они делают и где их разместить для нестабильных операций чтения / записи для JMM в качестве помощи авторам компиляторов, но этот текст также весьма полезен для других людей. Изменчивые операции чтения и записи дают одинаковые гарантии как в Java, так и в CLR, так что это обычно применимо.
Исходный код - прокрутите вниз до раздела "Барьеры памяти" (я бы скопировал интересные части, но форматирование не выдержит…)