Как замечено в этом вопросе: события Raising C# с дополнительным методом - являются этим плохо?
Я думаю об использовании этого дополнительного метода, чтобы безопасно сгенерировать событие:
public static void SafeRaise(this EventHandler handler, object sender, EventArgs e)
{
if (handler != null)
handler(sender, e);
}
Но Mike Rosenblum ставит этот вопрос в ответе Jon Skeet:
Парни необходимо добавить [MethodImpl (MethodImplOptions. NoInlining)] приписывают этим дополнительным методам, или иначе Ваша попытка скопировать делегата во временной переменной могла быть оптимизирована далеко Дрожанием, допуская исключение нулевой ссылки.
Я сделал некоторый тест в режиме Release, чтобы видеть, мог ли я получить состояние состязания, когда дополнительный метод не отмечен с NoInlining:
int n;
EventHandler myListener = (sender, e) => { n = 1; };
EventHandler myEvent = null;
Thread t1 = new Thread(() =>
{
while (true)
{
//This could cause a NullReferenceException
//In fact it will only cause an exception in:
// debug x86, debug x64 and release x86
//why doesn't it throw in release x64?
//if (myEvent != null)
// myEvent(null, EventArgs.Empty);
myEvent.SafeRaise(null, EventArgs.Empty);
}
});
Thread t2 = new Thread(() =>
{
while (true)
{
myEvent += myListener;
myEvent -= myListener;
}
});
t1.Start();
t2.Start();
Я запустил тест некоторое время в режиме Release и никогда не имел NullReferenceException.
Так, был Mike Rosenblum неправильно в его встраивании комментария и метода, не может вызвать состояние состязания?
На самом деле я предполагаю, что реальный вопрос, будет SaifeRaise быть встроенным как:
while (true)
{
EventHandler handler = myEvent;
if (handler != null)
handler(null, EventArgs.Empty);
}
или
while (true)
{
if (myEvent != null)
myEvent(null, EventArgs.Empty);
}
Проблема заключалась бы не во встраивании метода - это был бы JITter, выполняющий интересные вещи с доступом к памяти, вне зависимости от того, был ли он встроен.
Однако я не верю, что является проблемой в первую очередь. Несколько лет назад это было проблемой, но я считаю, что это было расценено как ошибочное прочтение модели памяти. Существует только одно логическое «чтение» переменной, и JITter не может оптимизировать это, так что значение изменяется между одним чтением копии и вторым чтением копии.
РЕДАКТИРОВАТЬ: Чтобы уточнить, я точно понимаю, почему это вызывает у вас проблемы. По сути, у вас есть два потока, изменяющих одну и ту же переменную (поскольку они используют захваченные переменные). Вполне возможно, что код будет выглядеть следующим образом:
Thread 1 Thread 2
myEvent += myListener;
if (myEvent != null) // No, it's not null here...
myEvent -= myListener; // Now it's null!
myEvent(null, EventArgs.Empty); // Bang!
Это немного менее очевидно в этом коде, чем обычно, поскольку переменная является захваченной переменной, а не обычным полем static / instance. Однако применяется тот же принцип.
Суть подхода безопасного повышения состоит в том, чтобы сохранить ссылку в локальной переменной, которую нельзя изменить из любых других потоков:
EventHandler handler = myEvent;
if (handler != null)
{
handler(null, EventArgs.Empty);
}
Теперь не имеет значения, изменяет ли поток 2 значение значение myEvent
- оно не может изменить значение обработчика, поэтому вы не получите исключение NullReferenceException
.
Если JIT делает встроенным SafeRaise
, он будет встроен в этот фрагмент, потому что встроенный параметр фактически становится новой локальной переменной. Проблема будет только в том случае, если JIT неправильно встроил его, сохранив два отдельных чтения myEvent
.
Теперь, что касается того, почему вы только видели, как это произошло в режиме отладки: я подозреваю, что с подключенным отладчиком гораздо больше места для потоков, чтобы прерывать друг друга. Возможно, произошла какая-то другая оптимизация - но она не привела к поломке , так что ничего страшного.
При правильном коде оптимизации не должны изменять его семантику. Следовательно, оптимизатор не может внести ошибку, если ошибки уже не было в коде.
Это проблема модели памяти.
В основном вопрос заключается в следующем: если мой код содержит только одно логическое чтение, может ли оптимизатор ввести другое чтение?
Удивительно, но ответ таков: возможно
В спецификации CLR ничто не мешает оптимизаторам выполнять это. Оптимизация не нарушает однопоточную семантику, и шаблоны доступа к памяти гарантированно сохраняются только для изменчивых полей (и даже это упрощение, которое не на 100% верно).
Таким образом, независимо от того, используете ли вы локальную переменную или параметр, код не является поточно-ориентированным .
Однако платформа Microsoft .NET документирует другую модель памяти. В этой модели оптимизатору не разрешено вводить чтения, и ваш код безопасен (независимо от оптимизации встраивания).
Тем не менее, использование [MethodImplOptions] кажется странным взломом, поскольку предотвращение ввода оптимизатором операций чтения является лишь побочным эффектом отказа от встраивания. Вместо этого я бы использовал изменчивое поле или Thread.VolatileRead.