Ответ 1
Я часто слышал, что в модели памяти .NET 2.0 записи всегда используют освободить заборы. Это правда?
Это зависит от того, к какой модели вы обращаетесь.
Во-первых, давайте точно определим барьер для защиты от забора. Выпуск семантики предусматривает, что никакие другие чтения или записи, появляющиеся перед барьером в последовательности команд, не могут двигаться после этого барьера.
- Спецификация ECMA имеет расслабленную модель, в которой записи не предоставляют эту гарантию.
- Было указано, что реализация CLR, предоставляемая Microsoft, усиливает модель, делая записи имеющими семантику освобождения.
- Архитектуры x86 и x64 укрепили модель, сделав записи барьерами, препятствующими выпуску, и читали барьеры заборного ограждения.
Таким образом, возможно, что другая реализация CLI (например, Mono), работающая на эзотерической архитектуре (например, ARM, для которой сейчас нацелена Windows 8), будет не предоставлять семантику релиза-освобождения при записи. Заметьте, что я сказал, что это возможно, но не обязательно. Но между всеми моделями памяти в игре, такими как разные программные и аппаратные уровни, вам нужно закодировать самую слабую модель, если вы хотите, чтобы ваш код был действительно портативным. Это означает кодирование модели ECMA и отсутствие каких-либо предположений.
Мы должны сделать список слоев модели памяти в игре просто явным.
- Компилятор: С# (или VB.NET или что-то еще) может перемещать инструкции.
- Runtime: Очевидно, что время выполнения CLI через JIT-компилятор может перемещать инструкции.
- Оборудование: И, конечно же, процессор и архитектура памяти также входят в игру.
Означает ли это, что даже без явных барьеров памяти или замков это невозможно наблюдать частично построенный объект (учитывая только ссылочные типы) на нить, отличную от той, на которой она создано?
Да (квалифицированный): Если среда, в которой работает приложение, является достаточно узкой, тогда может быть возможно наблюдать частично сконструированный экземпляр из другого потока. Это одна из причин, по которой дважды проверенный шаблон блокировки будет небезопасным без использования volatile
. В действительности, однако, я сомневаюсь, что вы столкнулись с этим главным образом из-за того, что реализация CLI в Microsoft не изменит порядок инструкций таким образом.
Было бы возможно, если следующий код будет наблюдать за любым выходом кроме "John 20" и "Jack 21", например "null 20" или "Jack 0"?
Опять же, это квалификация да. Но по той или иной причине, как выше, я сомневаюсь, что вы когда-нибудь будете наблюдать такое поведение.
Хотя, я должен отметить, что, поскольку person
не помечен как volatile
, возможно, что ничего не печатается вообще, потому что поток чтения всегда может видеть его как null
. В действительности, однако, я уверен, что вызов Console.WriteLine
заставит компиляторы С# и JIT избежать операции подъема, которая в противном случае могла бы перемещать чтение person
вне цикла. Я подозреваю, что вы уже хорошо знаете об этом нюансе.
Означает ли это, что я могу просто сделать все общие поля глубоко-неизменяемые ссылочные типы нестабильны и (в большинстве случаев) с моей работой?
Я не знаю. Это довольно загруженный вопрос. Мне неудобно отвечать в любом случае без лучшего понимания контекста, стоящего за ним. Я могу сказать, что я обычно избегаю использования volatile
в пользу более явных инструкций памяти, таких как операции Interlocked
, Thread.VolatileRead
, Thread.VolatileWrite
и Thread.MemoryBarrier
. Опять же, я также стараюсь избежать кода без блокировки вообще в пользу механизмов синхронизации более высокого уровня, таких как lock
.
Update:
Один из способов, которым я хотел бы визуализировать вещи, - предположить, что компилятор С#, JITer и т.д. оптимизируют как можно более агрессивно. Это означает, что Person.ctor
может быть кандидатом на inlining (поскольку он прост), который даст следующий псевдокод.
Person ref = allocate space for Person
ref.Name = name;
ref.Age = age;
person = instance;
DoSomething(person);
И поскольку в спецификации ECMA не существует семантики релиз-забора, тогда другие чтения и записи могут "плавать" ниже назначения до person
, давая следующую допустимую последовательность инструкций.
Person ref = allocate space for Person
person = ref;
person.Name = name;
person.Age = age;
DoSomething(person);
Итак, в этом случае вы можете увидеть, что person
назначается до его инициализации. Это справедливо, поскольку с точки зрения исполняемого потока логическая последовательность остается согласованной с физической последовательностью. Никаких непредвиденных побочных эффектов нет. Но по причинам, которые должны быть очевидны, эта последовательность будет катастрофической для другого потока.