Когда операции записи/чтения влияют на основную память?
-
20-09-2019 - |
Вопрос
Когда я записываю значение в поле, какие гарантии я получаю относительно того, когда новое значение будет сохранено в основной памяти?Например, как я узнаю, что процессор не сохранил новое значение в своем личном кеше, а обновил основную память?
Другой пример:
int m_foo;
void Read() // executed by thread X (on processor #0)
{
Console.Write(m_foo);
}
void Write() // executed by thread Y (on processor #1)
{
m_foo = 1;
}
Есть ли вероятность, что после Писать() было завершено выполнение, выполняется какой-то другой поток Читать() но на самом деле будет видеть «0» в качестве текущего значения?(поскольку, возможно, предыдущая запись в m_foo еще не была сброшена?).
Какие примитивы (помимо блокировок) доступны для обеспечения очистки записей?
РЕДАКТИРОВАТЬ
В примере кода, который я использовал, запись и чтение выполняются разными методами.Разве Thread.MemoryBarrier не влияет только на переупорядочение инструкций, существующих в той же области?
Кроме того, давайте предположим, что они не будут встроены JIT, как я могу быть уверен, что значение, записанное в m_foo, будет храниться не в регистре, а в основной памяти?(или когда m_foo читается, оно не получит старое значение из кэша ЦП).
Можно ли добиться этого без использования блокировок или ключевого слова «летучий»?(также предположим, что я использую не примитивные типы, а структуры размером WORD [настолько изменчивый, что нельзя применить].)
Решение
Volatile и Interlocked уже упоминались, вы просили примитивы, одно дополнение к списку - использовать Thread.MemoryBarrier()
прежде чем вы начнете писать или читать.Это гарантирует отсутствие переупорядочения операций записи и чтения в памяти.
Это делается «вручную», что lock
, Interlocked
и volatile
большую часть времени может выполняться автоматически.Вы можете использовать это как полную замену любого другого метода, но это, пожалуй, самый трудный путь, и так говорит MSDN:
«Трудно создать правильные многопоточные программы с помощью MemoryBarrier.Для большинства целей оператор C# Lock, оператор Visual Basic Synclock и методы класса монитора обеспечивают более простые и менее подверженные ошибкам способы синхронизации доступа к памяти.Мы рекомендуем вам использовать их вместо MemoryBarrier."
Как использовать MemoryBarrier
Прекрасным примером являются реализации VolatileRead
и VolatileWrite
, что оба внутренне используют MemoryBarrier
.Основное правило, которого следует придерживаться:когда вы читаете переменную, установите барьер памяти после чтение.Когда вы пишете значение, должен наступить барьер памяти до запись.
Если вы сомневаетесь, что это менее эффективно, то lock
, учтите, что блокировка - это не что иное, как «полное ограждение», поскольку она устанавливает барьер памяти до и после блока кода (на мгновение игнорируя Monitor).Этот принцип хорошо объяснен в этой отличная исчерпывающая статья Альбахари о потоках, блокировках, энергозависимости и барьерах памяти..
Из отражателя:
public static void VolatileWrite(ref byte address, byte value)
{
MemoryBarrier();
address = value;
}
public static byte VolatileRead(ref byte address)
{
byte num = address;
MemoryBarrier();
return num;
}
Другие советы
Если вы хотите, чтобы оно было написано быстро и правильно, отметьте его как volatile
, или (с большей болью) используйте Thread.VolatileRead
/ Thread.VolatileWrite
(непривлекательный вариант, его легко упустить, что делает его бесполезным).
volatile int m_foo;
В противном случае у вас практически нет никаких гарантий (как только вы говорите в нескольких потоках).
Возможно, вы также захотите посмотреть блокировку (Monitor
) или Interlocked
, что позволит добиться того же эффекта пока тот такой же подход используется со стороны любого доступа (т.е.все lock
, или все Interlocked
, и т. д).
Пока вы не используете синхронизацию, у вас нет гарантии, что поток, работающий на одном процессоре, увидит изменения, внесенные другим потоком, работающим на другом процессоре.Это связано с тем, что значение может быть кэшировано в кэше ЦП или в регистре ЦП.
Поэтому вам необходимо либо пометить переменную как изменчивую.Это создаст реализацию «происходит до» между чтением и записью.
Это не проблема с кэшем процессора.Запись обычно является сквозной (запись осуществляется как в кеш, так и в основную память), и все операции чтения будут иметь доступ к кешу.Но на пути есть много других кэшей (язык программирования, библиотеки, операционная система, ввод/вывод буферы и др.).Компилятор также может выбрать сохранение переменной в регистре процессора и никогда не записывать ее в основную память (это то, для чего предназначен оператор Летучий, избегайте сохранения значения в регистре, когда это может быть ввод-вывод, отображаемый в памяти).
Если у вас несколько процессов или несколько потоков, и синхронизация является проблемой, вы должны сделать это явно, есть много способов сделать это в зависимости от варианта использования.
Для однопоточной программы плевать, компилятор сделает то, что должен, и при чтении получит доступ к написанному.