Когда запись / чтение влияет на основную память?

Когда я записываю значение в поле, какие гарантии я получаю относительно того, когда новое значение будет сохранено в основной памяти? Например, как узнать, что процессор не сохраняет новое значение в своем частном кеше, а обновляет основную память?
Другой пример:

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;
}

Есть ли вероятность, что после завершения выполнения Write () какой-то другой поток выполнит Read (), но на самом деле увидит «0» в качестве текущего значения? (поскольку, возможно, предыдущая запись в m_foo еще не была сброшена?).
Какие примитивы (помимо блокировок) доступны для обеспечения сброса записи?


ИЗМЕНИТЬ
В примере кода, который я использовал, для записи и чтения используются разные методы. Разве Thread.MemoryBarrier не влияет только на перезапись инструкций, существующих в той же области?

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

Можно ли добиться этого без использования блокировок или ключевого слова volatile? (также предположим, что я использую не примитивные типы, а структуры размером WORD [поэтому volatile нельзя применять].)


person Community    schedule 14.11.2009    source источник
comment
Если у вас есть структуры размера WORD, вы можете преобразовать их в uint и применить volatile или любой другой метод, обсуждаемый в различных ответах.   -  person Abel    schedule 14.11.2009


Ответы (4)


Volatile и Interlocked уже упоминались, вы просили примитивы, одно дополнение к списку - использовать Thread.MemoryBarrier() перед записью или чтением. Это гарантирует отсутствие переупорядочения операций чтения и записи в память.

Это делает "вручную" то, что lock, Interlocked и volatile могут делать автоматически большую часть времени. Вы можете использовать это как полную замену любому другому методу, но это, пожалуй, самый сложный путь, и поэтому MSDN говорит:

«Трудно создавать правильные многопоточные программы с использованием MemoryBarrier. Для большинства целей оператор блокировки C #, оператор SyncLock Visual Basic и методы класса Monitor предоставляют более простые и менее подверженные ошибкам способы синхронизации обращений к памяти. . Мы рекомендуем использовать их вместо 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;
}
person Abel    schedule 14.11.2009
comment
В примере кода, который я использовал, для записи и чтения используются разные методы. Разве Thread.MemoryBarrier не влияет только на перезапись инструкций, существующих в той же области? Кроме того, предположим, что они не будут встроены JIT, как я могу убедиться, что значение, записанное в m_foo, будет храниться не в регистре, а в основной памяти? (или при чтении m_foo оно не получит старое значение из кеша ЦП). Можно ли добиться этого без использования блокировок или ключевого слова volatile? - person ; 14.11.2009
comment
Да, и да, вы можете использовать для этого MemoryBarrier. Но целесообразно ли это, это другое. Я дополню свой вопрос объяснением этого бита. - person Abel; 14.11.2009
comment
Так что практически, если я использую забор после записи нового значения, гарантировано, что новое значение будет перемещено из регистра в основную память? и если я использую забор перед чтением значения, тогда он фактически делает недействительным кеш, заставляя ЦП также получать значение из основной памяти? - person ; 14.11.2009
comment
да. Я не знаю, действительно ли это делает недействительным кеш ЦП, но гарантируется, что любые инструкции, записывающие на адрес, будут завершены, когда вы читаете с этого адреса (если чтение выполняется после MemoryBarrier). Вот хорошее дополнительное чтение, объясняющее этот принцип в более общих чертах: en.wikipedia.org/wiki/Memory_barrier < / а> - person Abel; 14.11.2009

Если вы хотите, чтобы он был написан быстро и по порядку, пометьте его как _ 1_ или (с большей болью) используйте _ 2_ / _3 _ (вариант не очень привлекательный, и его легко пропустить, что делает его бесполезным).

volatile int m_foo;

В противном случае у вас практически нет никаких гарантий (как только вы говорите в нескольких потоках).

Вы также можете посмотреть на блокировку (Monitor) или Interlocked, что даст тот же эффект до тех пор, пока тот же подход используется для любого доступа (т. е. все _ 7_ или все Interlocked и т. д.).

person Marc Gravell    schedule 14.11.2009
comment
Volatile на самом деле даже не гарантирует оперативность - только порядок. Я решил, что не совсем понимаю volatile ... вся модель памяти просто сбивает с толку. Я буду кодировать без блокировки только тогда, когда он использует другой фреймворк, например PFX ... - person Jon Skeet; 14.11.2009
comment
Примечание по volatile: этот порядок не гарантируется, когда чтение следует за записью, что делает volatile менее идеальным для задания, как можно было бы ожидать. Это малоизвестный факт, но гарантия катастрофы. Среди прочего, это объясняется здесь: albahari.com/threading/part4.aspx#_Memory_Barriers < / а> - person Abel; 14.11.2009
comment
Другими словами, если вам нужно чтение после записи, вы должны использовать полное ограждение и использовать Interlocked (использует полное ограждение) или традиционную блокировку. - person Abel; 14.11.2009
comment
@Abel: Я имел в виду только порядок записи. - person Jon Skeet; 14.11.2009
comment
@Jon: и я не видел вашего текста до того, как отправил. Я просто хотел сделать общее замечание об использовании volatile ;-) - person Abel; 14.11.2009
comment
Разве «энергозависимость» (и ограничения памяти в целом) не влияет только на порядок инструкций или также влияет на то, когда значения сбрасываются из регистров в основную память? (или заставляя ЦП получать значение из основной памяти, а не из частного кеша). Можно ли указать процессору читать значение из основной памяти без использования ключевого слова volatile? (скажем, у меня есть 4-байтовая структура вместо примитивного типа. Что тогда? - person ; 14.11.2009

Пока вы не используете синхронизацию, у вас нет гарантии, что поток, работающий на одном процессоре, увидит изменения, сделанные другим потоком, выполняющимся на другом процессоре. Это потому, что значение может быть кэшировано в кешах ЦП или в регистре ЦП.

Поэтому вам нужно либо пометить переменную как изменчивую. Это создаст реальность «Произойдет до» между чтением и записью.

person Gamlor    schedule 14.11.2009

Это не проблема кэша процессора. Записи обычно являются сквозными (записи идут как в кеш, так и в основную память), и все чтения будут иметь доступ к кешу. Но на подходе много других кешей (язык программирования, библиотеки, операционная система, ввод-вывод буферы и т. д.). Компилятор также может сохранить переменную в регистре процессора и никогда не записывать ее в основную память (это то, для чего предназначен изменчивый оператор, избегайте сохранения значения в регистре, когда это может быть отображаемый в память ввод-вывод).

Если у вас есть несколько процессов или несколько потоков, и синхронизация является проблемой, вы должны делать это явно, есть много способов сделать это в зависимости от варианта использования.

Для однопоточной программы не беспокойтесь, компилятор сделает то, что должен, а чтение будет иметь доступ к тому, что было написано.

person kriss    schedule 14.11.2009