Модель памяти .NET, изменчивые переменные и test-and-set: что гарантировано?
Я знаю, что модель памяти .NET(на .NET Framework, а не compact/micro/silverlight/mono/xna/what-have-you) гарантировала, что для определенных типов (в первую очередь примитивных целых и справочных) операций были гарантированный атомный.
Кроме того, я считаю, что команда test-and-set x86/x64 (и Interlocked.CompareExchange
) фактически ссылается на местоположение глобальной памяти, поэтому, если ей удастся, другой Interlocked.CompareExchange
увидит новое значение.
Наконец, я считаю, что ключевое слово volatile
- это инструкция для компилятора для распространения чтения и записи ASAP и не изменять порядок операций с этой переменной (правильно?).
Это приводит к нескольким вопросам:
- Я верю ли мои убеждения?
-
Interlocked.Read
не имеет перегрузки для int, только для longs (которые являются 2 WORD и, следовательно, обычно не читаются атомарно). Я всегда предполагал, что модель памяти .NET гарантирует, что самое новое значение будет видно при чтении int/ссылок, однако с кэшами процессора, регистрами и т.д. Я начинаю видеть, что это может быть невозможно. Итак, есть способ принудительно переназначить переменную?
- Неустойчиво ли достаточно для решения вышеуказанной проблемы для целых чисел и ссылок?
- В x86/x64 я могу предположить, что...
Если есть две глобальные целочисленные переменные x и y, обе инициализируются до 0, если я пишу:
x = 1;
y = 2;
Этот поток NO будет видеть x = 0 и y = 2 (т.е. записи будут происходить по порядку). Изменяется ли это, если они нестабильны?
Ответы
Ответ 1
- Только чтение и запись в переменные шириной не более 32 бит (и 64 бит в ширину в системах x64) являются атомарными. Все это означает, что вы не будете читать int и получите полузаписанное значение. Это не значит, что арифметика атомарна.
- Блокированные операции также действуют как барьеры памяти, поэтому да,
Interlocked.CompareExchange
увидит обновленное значение.
- Смотрите эту страницу. Летучие не означает упорядоченные. Некоторые компиляторы могут не переупорядочивать операции с изменчивыми переменными, но ЦП может свободно переупорядочивать. Если вы хотите остановить процессор от повторного заказа, используйте (полный) барьер памяти.
- Модель памяти гарантирует, что чтение и запись являются атомарными, а использование ключевого слова volatile гарантирует, что чтение всегда будет поступать из памяти, а не из регистра. Таким образом, вы увидите новейшее значение. Это связано с тем, что процессоры x86 при необходимости недействительны в кэше - см. this и это. Кроме того, см. InterlockedCompareExchange64 для того, чтобы атомарно читать 64-битные значения.
- И, наконец, последний вопрос. Ответ - это поток, который на самом деле может видеть
x = 0
и y = 2
, а использование ключевого слова volatile не изменяет этого, потому что CPU может свободно переупорядочивать инструкции. Вам нужен барьер памяти.
Резюме:
- Компилятор может свободно перенаправлять инструкции.
- ЦП может свободно переупорядочивать инструкции.
- Чтение и запись в формате Word являются атомарными. Арифметические и другие операции не являются атомарными, потому что они включают чтение, вычисление, а затем запись.
- Чтение в формате Word из памяти всегда будет получать самое новое значение. Но большую часть времени вы не знаете, действительно ли вы читаете из памяти.
- Полный барьер памяти останавливается (1) и (2). Большинство компиляторов позволяют вам останавливать (1) самостоятельно.
- Ключевое слово volatile гарантирует, что вы читаете из памяти - (4).
- Блокированные операции (префикс блокировки) позволяют выполнять несколько операций атомарно. Например, чтение + запись (InterlockedExchange). Или прочитайте + сравните + напишите (InterlockedCompareExchange). Они также действуют как барьеры памяти, поэтому (1) и (2) останавливаются. Они всегда записываются в память (очевидно), поэтому (4) обеспечивается.
Ответ 2
Перешел через этот старый поток. Ответы от Hans и wj32 правильны, за исключением части, касающейся volatile
.
В частности, относительно вашего вопроса
На x86/x64 можно предположить, что... Если существует две глобальные целочисленные переменные x и y, обе инициализированы до 0, если if Я пишу: x = 1; y = 2;
Этот поток NO будет видеть x = 0 и y = 2 (т.е. записи будут происходят в порядке). Означает ли это, если они неустойчивы?
Если y
является изменчивым, запись в x
является гарантией, которая должна произойти до записи в y
, поэтому нить никогда не увидит x = 0
и y = 2
. Это связано с тем, что запись в изменчивую переменную имеет "семантику выпуска" (логически эквивалентную эмиссии заборного забора), то есть все инструкции чтения/записи до того, как они не будут перемещаться. (Это означает, что если x является изменчивым, но y нет, вы можете увидеть неожиданные x = 0
и y = 2
.) См. Пример описания и кода в С# spec для получения более подробной информации.
Ответ 3
Нет, ключевое слово volatile и гарантия атомарности слишком слабые. Для этого вам нужен барьер памяти. Вы можете получить его явно с помощью метода Thread.MemoryBarrier().