Когда записи / чтения влияют на основную память? - PullRequest
5 голосов
/ 14 ноября 2009

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

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 еще не была очищена?).
Какие примитивы (кроме блокировок) доступны для обеспечения сброса записей?


EDIT
В примере кода, который я использовал, запись и чтение размещаются по-разному. Разве Thread.MemoryBarrier не влияет только на запись инструкций, которые существуют в той же области видимости?

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

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

Ответы [ 4 ]

11 голосов
/ 14 ноября 2009

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

volatile int m_foo;

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

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

3 голосов
/ 14 ноября 2009

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

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

"Трудно построить правильные многопоточные программы, используя MemoryBarrier. Для большинства целей Оператор блокировки C #, Visual Basic Заявление SyncLock и методы класс 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;
}
2 голосов
/ 14 ноября 2009

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

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

2 голосов
/ 14 ноября 2009

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

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

...