Я спросил этот вопрос и получил strong > интересный (и немного обескураживающий) ответ.
Даниэль заявляет в своем ответе (если я не читаю его неправильно), что спецификация ECMA-335 CLI может позволить компилятору сгенерировать код, который выдает NullReferenceException
из следующего DoCallback
метод.
class MyClass {
private Action _Callback;
public Action Callback {
get { return _Callback; }
set { _Callback = value; }
}
public void DoCallback() {
Action local;
local = Callback;
if (local == null)
local = new Action(() => { });
local();
}
}
Он говорит, что для того, чтобы гарантировать, что NullReferenceException
не выбрано, ключевое слово volatile
должно использоваться на _Callback
или lock
должно использоваться вокруг строки local = Callback;
.
Может ли кто-нибудь подтвердить это? И, если это правда, есть ли разница в поведении между компиляторами Моно и .NET по этой проблеме?
Изменить
Вот ссылка на standard.
Обновить
Я думаю, что это уместная часть спецификации (12.6.4):
Соответствующие реализации CLI могут свободно выполнять программы используя любую технологию, которая гарантирует, что в одном потоке выполнение, что побочные эффекты и исключения, генерируемые потоком, являются видимый в порядке, указанном CIL. Только для этой цели неустойчивые операции (включая изменчивые чтения) побочные эффекты. (Обратите внимание, что хотя только летучие операции составляют видимые побочные эффекты, нестабильные операции также влияют на видимость из энергонезависимых ссылок.) Неустойчивые операции указаны в §12.6.7. Отсутствуют заказывающие гарантии относительно исключений впрыскивается в нить другой нитью (такие исключения иногда называемые "асинхронными исключениями" (например, System.Threading.ThreadAbortException).
[Обоснование: оптимизация компилятор может свободно переупорядочивать побочные эффекты и синхронные исключения для в той степени, в которой это переупорядочение не изменяет никакой наблюдаемой программы поведение. конечное обоснование]
[Примечание: реализация CLI разрешено использовать оптимизирующий компилятор, например, для преобразования CIL на собственный машинный код при условии, что компилятор поддерживает (в каждом одиночный поток исполнения) тот же порядок побочных эффектов и синхронные исключения.
Итак... Мне интересно, поддерживает ли этот оператор компилятор для оптимизации свойства Callback
(который обращается к простому полю) и переменной local
, чтобы создать следующее, которое имеет тот же поведение в одном потоке выполнения:
if (_Callback != null) _Callback();
else new Action(() => { })();
Раздел 12.6.7 в ключе volatile
, похоже, предлагает решение для программистов, желающих избежать оптимизации:
Волатильное чтение имеет "приобретать семантику", что означает, что чтение гарантированно произойдет до любых ссылок на память, которые происходят после инструкция чтения в последовательности команд CIL. Неустойчивая запись имеет "семантику выпуска", что означает, что запись гарантирована после любых ссылок на память до начала записи в CIL последовательность команд. Соответствующая реализация CLI должна гарантируют эту семантику нестабильных операций. Это гарантирует, что все потоки будут наблюдать волатильную запись, выполняемую любым другим потоком в порядок, в котором они были выполнены. Но соответствующая реализация не требуется, чтобы обеспечить единый общий порядок волатильной записи, как видно из всех потоков исполнения. Оптимизирующий компилятор, который преобразует CIL на собственный код не должен удалять какую-либо изменчивую работу, он объединяет несколько летучих операций в одну операцию.