Ответ 1
Согласно источникам, которые вы предоставили, и некоторым другим в прошлом, это сводится к следующему:
-
При реализации Microsoft вы можете положиться на not, ознакомившись с введением [1] [2] [3]
-
Для любой другой реализации, может читать введение, если не указано иное
EDIT: внимательно прочитав спецификацию ECMA CLI, ознакомьтесь с инструкциями, но с ограничениями. Из раздела I, 12.6.4 Оптимизация:
Соответствующие реализации CLI могут свободно запускать программы с использованием любой технологии, которая гарантирует в одном потоке выполнения побочные эффекты и исключения, созданные потоком, видны в порядке, указанном CIL. Для этой цели только изменчивые операции (включая изменчивые чтения) представляют собой видимые побочные эффекты. (Обратите внимание, что, хотя только изменчивые операции представляют собой видимые побочные эффекты, летучие операции также влияют на видимость нелетучих ссылок.)
Очень важная часть этого параграфа находится в круглых скобках:
Обратите внимание, что, хотя только изменчивые операции представляют собой видимые побочные эффекты, летучие операции также влияют на видимость нелетучих ссылок.
Итак, если сгенерированный CIL читает поле только один раз, реализация должна вести себя одинаково. Если он вводит чтения, это потому, что он может доказать, что последующие чтения будут давать тот же результат, даже если речь идет о побочных эффектах других потоков. Если он не может доказать это, и он все еще вводит чтения, это ошибка.
Таким же образом, С# язык также ограничивает чтение введением на уровне С# -to-CIL. Из спецификации языка С# версии 5.0, 3.10 Порядок выполнения:
Выполнение программы С# продолжается так, что побочные эффекты каждого исполняемого потока сохраняются в критических точках выполнения. побочный эффект определяется как чтение или запись изменчивого поля, запись в энергонезависимую переменную, запись на внешний ресурс и металирование исключения. Критические точки выполнения, в которых должен сохраняться порядок этих побочных эффектов, - это ссылки на изменчивые поля (§10.5.3),
lock
(§8.12), а также создание и прекращение потоков. Среда исполнения может изменять порядок выполнения программы С# с учетом следующих ограничений:
Зависимость данных сохраняется в потоке выполнения. То есть значение каждой переменной вычисляется так, как будто все инструкции в потоке выполнялись в исходном программном порядке.
Правила упорядочения инициализации сохраняются (§10.5.4 и §10.5.5).
Упорядочение побочных эффектов сохраняется относительно неустойчивых чтений и записи (§10.5.3). Кроме того, среда выполнения не должна оценивать часть выражения, если она может вывести, что значение этих выражений не используется и что не создаются необходимые побочные эффекты (в том числе вызванные вызовом метода или доступом к нестабильному полю). Когда выполнение программы прерывается асинхронным событием (например, исключением, созданным другим потоком), не гарантируется, что наблюдаемые побочные эффекты видны в исходном порядке программы.
Точка о зависимости данных - это та, которую я хочу подчеркнуть:
Зависимость данных сохраняется в потоке выполнения. То есть значение каждой переменной вычисляется так, как если бы все операторы в потоке выполнялись в исходном программном порядке.
Таким образом, глядя на ваш пример (аналогичный тому, который дал Игорь Островский [4])
EventHandler localCopy = SomeEvent;
if (localCopy != null)
localCopy(this, args);
Компилятор С# не должен выполнять чтение, когда-либо. Даже если это может доказать, что не существует мешающих доступов, нет гарантии от базового CLI, что два последовательных нелетучих чтения на SomeEvent
будут иметь одинаковый результат.
Или, используя эквивалентный оператор с нулевым условием, поскольку С# 6.0:
SomeEvent?.Invoke(this, args);
Компилятор С# должен всегда расширяться до предыдущего кода (гарантируя уникальное имя неконфликтной переменной), не выполняя чтение, поскольку это оставило бы состояние гонки.
Компилятор JIT должен выполнять только чтение, если он может доказать, что в зависимости от базовой аппаратной платформы нет мешающих доступов, так что два последовательных энергонезависимых чтения на SomeEvent
фактически будут иметь одинаковый результат, Это может быть не так, если, например, значение не сохраняется в регистре и если кеш может быть сброшен между чтениями.
Такая оптимизация, если она локальна, может выполняться только на простых (не ref и без вывода) параметрах и незахваченных локальных переменных. Благодаря оптимизации между методами или целыми программами он может выполняться на общих полях, параметрах ref или out и захваченных локальных переменных, которые могут быть доказаны, что они никогда не будут заметно затронуты другими потоками.
Таким образом, существует большая разница в том, записывает ли он следующий код или компилятор С#, генерирующий следующий код, по сравнению с JIT-компилятором, генерирующим машинный код, эквивалентный следующему коду, поскольку JIT-компилятор является единственным, способным доказать, введенное чтение согласуется с выполнением одного потока, даже сталкиваясь с потенциальными побочными эффектами, вызванными другими потоками:
if (SomeEvent != null)
SomeEvent(this, args);
Введенное чтение, которое может дать другой результат, - это ошибка, даже в соответствии со стандартом, поскольку наблюдаемая разница была кодом, выполняемым в программном порядке без введенного чтения.
Таким образом, если комментарий в примере Игоря Островского [4], я говорю это ошибка.
[1]: комментарий Эрика Липперта; процитировать:
Чтобы рассказать о спецификации ECI CLI и спецификации С#: более мощная модель памяти promises, созданная с помощью CLR 2.0, - это promises, сделанная Microsoft. Третья сторона, решившая сделать свою собственную реализацию С#, которая генерирует код, который выполняется на собственной реализации CLI, может выбрать более слабую модель памяти и по-прежнему соответствовать спецификациям. Независимо от того, сделала ли это команда Mono, я не знаю; вы должны спросить их.
[2]: модель памяти CLR 2.0 Джо Даффи, повторив следующую ссылку; цитируя соответствующую часть:
- Правило 1: Зависимость данных между грузами и магазинами никогда не нарушается.
- Правило 2: Все магазины имеют семантику выпуска, т.е. загрузка или сохранение могут перемещаться после одного.
- Правило 3: все летучие нагрузки приобретаются, т.е. загрузка или хранение не могут перемещаться до одного.
- Правило 4. Никакие нагрузки и хранилища никогда не пересекают полный барьер (например, Thread.MemoryBarrier, блокировка, Interlocked.Exchange, Interlocked.CompareExchange и т.д.).
- Правило 5: Загрузка и сохранение в кучу никогда не могут быть введены.
- Правило 6. Нагрузки и хранилища могут быть удалены только при объединении смежных нагрузок и хранилищ из/в том же месте.
[3]: Понимание влияния методов с низким уровнем блокировки в многопоточных приложениях Vance Morrison, последний снимок, который я мог бы получить на Интернет-архив; цитируя соответствующую часть:
Сильная модель 2:.NET Framework 2.0
(...)
- Все правила, содержащиеся в модели ECMA, в частности три основных правила модели памяти, а также правила ECMA для неустойчивых.
- Чтение и запись не могут быть введены.
- Чтение может быть удалено только в том случае, если оно смежно с другим, прочитанным в том же месте из того же потока. Запись может быть удалена только в том случае, если она смежна с другой записью в том же месте из того же потока. Правило 5 может использоваться для чтения или записи рядом, прежде чем применять это правило.
- Писания не могут перемещаться из других записей из одного потока.
- Чтения могут перемещаться только по времени, но никогда не записываются в одну ячейку памяти из того же потока.
[4]: С# - модель памяти С# в теории и практике, часть 2 Игоря Островского, где он показывает прочитанное введение например, что, по его словам, JIT может выполнять так, что два последующих чтения могут иметь разные результаты; цитируя соответствующую часть:
Чтение ВведениеКак я только что объяснил, компилятор иногда сплавляет несколько чтений в один. Компилятор также может разбивать одно чтение на несколько чтений. В .NET Framework 4.5 ознакомление с введением гораздо реже, чем чтение, и происходит только в очень редких особых обстоятельствах. Однако иногда это происходит.
Чтобы понять введение в чтение, рассмотрите следующий пример:
public class ReadIntro {
private Object _obj = new Object();
void PrintObj() {
Object obj = _obj;
if (obj != null) {
Console.WriteLine(obj.ToString());
// May throw a NullReferenceException
}
}
void Uninitialize() {
_obj = null;
}
}
Если вы исследуете метод PrintObj, это похоже на то, что значение obj никогда не будет равно null в выражении obj.ToString. Однако эта строка кода может фактически вызвать исключение NullReferenceException. CLR JIT может скомпилировать метод PrintObj, как если бы он был написан следующим образом:
void PrintObj() {
if (_obj != null) {
Console.WriteLine(_obj.ToString());
}
}
Поскольку чтение поля _obj было разделено на два чтения поля, теперь метод ToString может быть вызван нулевой целью.
Обратите внимание, что вы не сможете воспроизвести исключение NullReferenceException с использованием этого примера кода в .NET Framework 4.5 на x86-x64. Прочитать введение очень сложно воспроизвести в .NET Framework 4.5, но оно тем не менее происходит в определенных особых обстоятельствах.