Thread.VolatileRead Реализация
Я смотрю на реализацию методов VolatileRead/VolatileWrite (используя Reflector), и я чем-то озадачен.
Это реализация для VolatileRead:
[MethodImpl(MethodImplOptions.NoInlining)]
public static int VolatileRead(ref int address)
{
int num = address;
MemoryBarrier();
return num;
}
Как получается, что барьер памяти размещается после считывания значения "адрес"? разве не должно быть наоборот? (поместите перед чтением значения, поэтому любые ожидающие записи в "адрес" будут завершены к тому моменту, когда мы осуществим фактическое чтение. То же самое относится и к VolatileWrite, где барьер памяти находится перед присвоением значения. Почему это так? Кроме того, почему эти методы имеют атрибут NoInlining? Что может произойти, если они были встроены?
2 ответа
Я думал, что до недавнего времени. Изменчивые чтения - это не то, о чем вы думаете - они не о том, чтобы гарантировать, что они получают самую последнюю ценность; они предназначены для того, чтобы убедиться, что никакое чтение, которое находится позже в программном коде, не будет перемещено до этого чтения. Это то, что гарантирует спецификация - и аналогично для изменчивых записей, это гарантирует, что ни одна более ранняя запись не будет перенесена после изменчивой.
Вы не одиноки, подозревая этот код, но Джо Даффи объясняет это лучше, чем я:)
Мой ответ на это состоит в том, чтобы отказаться от кодирования без блокировки, кроме использования таких вещей, как PFX, которые предназначены для того, чтобы изолировать меня от этого. Модель памяти слишком сложна для меня - я оставлю это экспертам и останусь с вещами, которые, как я знаю, безопасны.
Однажды я обновлю свою статью о потоках, чтобы отразить это, но я думаю, что мне нужно сначала обсудить ее более разумно...
(Между прочим, я не знаю о части без встраивания. Я подозреваю, что встраивание может привести к некоторым другим оптимизациям, которые не должны происходить при нестабильном чтении / записи, но я легко могу ошибаться...)
Может быть, я упрощаю, но я думаю, что объяснения о переупорядочении и когерентности кэша и т. Д. Дают слишком много деталей.
Итак, почему MemoryBarrier приходит после фактического чтения? Я попытаюсь объяснить это на примере, который использует объект вместо int.
Кто-то может подумать, что правильно: поток 1 создает объект (инициализирует его внутренние данные). Затем поток 1 помещает объект в переменную. Затем он "делает забор" и все потоки видят новое значение.
Затем чтение выглядит примерно так: поток 2 "делает забор". Поток 2 читает экземпляр объекта. Поток 2 уверен, что у него есть все внутренние данные этого экземпляра (так как он начался с забора).
Самая большая проблема в этом: поток 1 создает объект и инициализирует его. Затем поток 1 помещает объект в переменную. До того, как поток очищает кэш, процессор сам очищает часть кэша... он фиксирует только адрес переменной (но не содержимое этой переменной).
В тот момент поток 2 уже очистил свой кэш. Так что это будет читать все из основной памяти. Итак, он читает переменную (она есть). Затем он читает содержимое (его там нет).
Наконец, после всего этого ЦПУ 1 выполняет поток 1, который выполняет забор.
Итак, что происходит с изменчивой записью и чтением? При энергозависимой записи содержимое объекта немедленно попадает в память (начинается с забора), затем они устанавливают переменную (может не сразу перейти в реальную память). Затем энергозависимое чтение сначала очистит кэш. Затем он читает поле. Если он получает значение при чтении поля, то несомненно, что содержимое, на которое указывает эта ссылка, действительно присутствует.
Этими мелочами, да, возможно, что вы делаете VolatileWrite(1), а другой поток все еще видит значение ноль. Но как только другие потоки увидят значение 1 (используя volatile read), все другие необходимые элементы, на которые можно сослаться, уже есть. Вы не можете этого сказать, поскольку, читая старое значение (0 или ноль), вы можете просто не прогрессировать, учитывая, что у вас все еще нет всего, что вам нужно.
Я уже видел некоторые дискуссии о том, что, даже если это дважды очищает кэш, правильная схема будет такой:
MemoryBarrier - сбросит другие переменные, измененные перед этим вызовом
Написать
MemoryBarrier - гарантирует, что запись была сброшена
Затем для чтения потребуется то же самое:
MemoryBarrier
Читайте - Гарантирует, что мы увидим самую свежую информацию... возможно, ту, которая была поставлена ПОСЛЕ нашего барьера памяти.
Поскольку что-то могло появиться после нашего MemoryBarrier и уже было прочитано, мы должны установить еще один MemoryBarrier для доступа к содержимому.
Это могут быть два Write-Fences или два Read-Fences, если они существуют в.Net.
Я не уверен во всем, что я сказал... это "компиляция" многих данных, которые я получил, и это действительно объясняет, почему VolatileRead и VolatileWrite кажутся противоположными, но это также гарантирует, что никакие недопустимые значения не читаются при их использовании.