Вопрос

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

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, и т. д).

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top