Подтвердить что ты не робот

Реализация Thread.VolatileRead

Я рассматриваю реализацию методов VolatileRead/VolatileWrite (используя Reflector), и я озадачен чем-то.

Это реализация для VolatileRead:

[MethodImpl(MethodImplOptions.NoInlining)]
public static int VolatileRead(ref int address)
{
    int num = address;
    MemoryBarrier();
    return num;
}

Каким образом помещается барьер памяти после считывания значения "адрес"? разве это не должно быть наоборот? (место перед чтением значения, поэтому любые ожидающие записи в "адрес" будут завершены к моменту начала фактического чтения. То же самое происходит и с VolatileWrite, где барьер памяти помещается перед назначением значения. Почему это? Кроме того, почему эти методы имеют атрибут NoInlining? что может произойти, если они были вложены?

4b9b3361

Ответ 1

Я думал, что до недавнего времени. Неустойчивые чтения - это не то, что вы думаете, это не гарантия того, что они получают самую последнюю ценность; они должны убедиться, что до этого чтения не прочитано, которое позже в программном коде перемещено. То, что гарантирует спецификация, а также для волатильной записи, гарантирует, что более ранняя запись не будет перенесена после летучего.

Вы не одиноки, подозревая этот код, но Джо Даффи объясняет это лучше, чем я могу:)

Мой ответ на это заключается в том, чтобы отказаться от кодирования без блокировки, кроме как используя такие вещи, как PFX, которые предназначены для того, чтобы изолировать меня от него. Модель памяти слишком сложна для меня - я оставлю ее экспертам и придерживаюсь того, что, как я знаю, безопасно.

Однажды я обновлю свою статью, чтобы отразить это, но я думаю, что мне нужно обсудить ее более разумно сначала...

(я не знаю о части без инкрустации, кстати, я подозреваю, что inlining может ввести некоторые другие оптимизации, которые не должны произойти вокруг изменчивых операций чтения/записи, но я мог бы легко ошибиться...)

Ответ 2

Возможно, я упрощаю, но я думаю, что объяснения о переупорядочении и согласованности кеша и т.д. дают слишком много деталей.

Итак, почему MemoryBarrier приходит после фактического чтения? Я попытаюсь объяснить это примером, который использует объект вместо int.

Можно подумать, что правильно: Тема 1 создает объект (инициализирует его внутренние данные). Затем поток 1 помещает объект в переменную. Затем он "делает забор", и все потоки видят новое значение.

Затем чтение выглядит примерно так: Тема 2 "делает забор". В потоке 2 читается экземпляр объекта. Thread 2 уверен, что он имеет все внутренние данные этого экземпляра (поскольку он начинался с забора).

Самая большая проблема с этим: Тема 1 создает объект и инициализирует его. Затем поток 1 помещает объект в переменную. Перед тем, как Thread сбрасывает кеш, сам процессор очищает часть кэша... он фиксирует только адрес переменной (а не содержимое этой переменной).

В этот момент Thread 2 уже сбросил кеш. Поэтому он будет читать все из основной памяти. Таким образом, он считывает переменную (она есть). Затем он считывает содержимое (его нет).

Наконец, после всего этого CPU 1 выполняет Thread 1, который делает забор.


Итак, что происходит с изменчивой записью и чтением? Волатильная запись приводит к тому, что содержимое объекта сразу переходит в память (начинается с забора), затем они устанавливают переменную (возможно, она не сразу переходит в реальную память). Затем волатильное чтение сначала очистит кеш. Затем он читает поле. Если он получает значение при чтении поля, он уверен, что содержимое, указанное этой ссылкой, действительно присутствует.


По этим мелочам, да, возможно, что вы делаете VolatileWrite (1), а другой поток все еще видите значение нуля. Но как только другие потоки видят значение 1 (используя волатильное чтение), все остальные элементы, на которые можно ссылаться, уже есть. Вы не можете это сказать, как при чтении старого значения (0 или null) вы можете просто не развиваться, учитывая, что у вас еще нет всего, что вам нужно.


Я уже видел некоторые дискуссии о том, что, даже если это дважды очищает кеши, правильный шаблон будет:
MemoryBarrier - сбросит другие переменные, измененные до этого вызова
Написать
MemoryBarrier - гарантирует, что запись была покрашена

Чтение тогда будет необходимо:
MemoryBarrier
Чтение - гарантирует, что мы увидим последнюю информацию... возможно, которая была поставлена ​​ПОСЛЕ нашей памяти.
Поскольку что-то появилось после нашего MemoryBarrier и было уже прочитано, мы должны добавить еще один MemoryBarrier для доступа к содержимому.

Это могут быть две записи-записи или два чтения, если они существуют в .Net.


Я не уверен во всем, что я сказал... это "компиляция" многих сведений, которые я получил, и это действительно объясняет, почему VolatileRead и VolatileWrite, похоже, меняются на противоположные, но также гарантируют, что недопустимые значения не читаются, когда используя их.