Может ли метод оптимизации встраивания вызывать условия гонки?
Как видно из этого вопроса:
Поднять события С# с помощью метода расширения - это плохо?
Я думаю об использовании этого метода расширения, чтобы безопасно поднять событие:
public static void SafeRaise(this EventHandler handler, object sender, EventArgs e)
{
if (handler != null)
handler(sender, e);
}
Но Майк Розенблюм поднимает эту озабоченность в ответ Джона Скита:
Вам, ребята, нужно добавить [MethodImpl (MethodImplOptions.NoInlining)] атрибут этих методов расширения или ваша попытка скопировать делегировать временную переменную оптимизироваться JITter, с учетом нулевой ссылки исключение.
Я провел несколько тестов в режиме 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.
Итак, был ли неправильный Майк Розенблюм в своем комментарии, а метод вложения не может вызвать расовое состояние?
На самом деле, я думаю, что реальный вопрос заключается в том, будет ли SaifeRaise быть в виде:
while (true)
{
EventHandler handler = myEvent;
if (handler != null)
handler(null, EventArgs.Empty);
}
или
while (true)
{
if (myEvent != null)
myEvent(null, EventArgs.Empty);
}
Ответы
Ответ 1
Проблема не заключалась бы в разработке метода - это был бы JITter, делающий интересные вещи с доступом к памяти независимо от того, был ли он встроен.
Однако я не считаю, что это проблема в первую очередь. Несколько лет назад это вызвало беспокойство, но я считаю, что это считалось ошибочным чтением модели памяти. Там только одно логическое "чтение" переменной, и JITTER не может оптимизировать это, так что значение изменяется между одним считыванием копии и вторым чтением копии.
РЕДАКТИРОВАТЬ: просто для того, чтобы уточнить, я точно понимаю, почему это вызывает проблемы для вас. У вас в основном есть два потока, изменяющих одну и ту же переменную (поскольку они используют захваченные переменные). Совершенно возможно, чтобы код имел место следующим образом:
Thread 1 Thread 2
myEvent += myListener;
if (myEvent != null) // No, it not null here...
myEvent -= myListener; // Now it null!
myEvent(null, EventArgs.Empty); // Bang!
Это немного менее очевидно в этом коде, чем обычно, поскольку переменная является захваченной переменной, а не нормальным статическим/экземпляром поля. Тот же принцип применяется, однако.
Точкой подхода безопасного подъема является сохранение ссылки в локальной переменной, которая не может быть изменена из других потоков:
EventHandler handler = myEvent;
if (handler != null)
{
handler(null, EventArgs.Empty);
}
Теперь не имеет значения, изменит ли поток 2 значение myEvent
- он не может изменить значение обработчика, поэтому вы не получите NullReferenceException
.
Если JIT делает inline SafeRaise
, он будет привязан к этому фрагменту, потому что встроенный параметр заканчивается как новая локальная переменная. Проблема была бы только в том случае, если JIT неправильно вложил ее, сохранив два отдельных чтения myEvent
.
Теперь о том, почему вы только видели, что это происходит в режиме отладки: я подозреваю, что при подключенном отладчике гораздо больше места для потоков, чтобы прервать друг друга. Возможно, произошла какая-то другая оптимизация - но она не вносила никаких поломки, так что все в порядке.
Ответ 2
Это проблема с моделью памяти.
В основном возникает вопрос: если мой код содержит только одно логическое чтение, может ли оптимизатор ввести другое чтение?
Удивительно, но ответ: возможно
В спецификации CLR ничто не мешает оптимизаторам это делать. Оптимизация не прерывает однопоточную семантику, и шаблоны доступа к памяти гарантируются только для летучих полей (и даже для упрощения, которое не является 100% истинным).
Таким образом, независимо от того, используете ли вы локальную переменную или параметр, код не является потокобезопасным.
Однако платформа Microsoft.NET документирует другую модель памяти. В этой модели оптимизатору не разрешается вводить чтения, а ваш код безопасен (независимо от оптимизации встраивания).
Тем не менее, использование [MethodImplOptions] кажется странным взломом, так как предотвращение введения оптимизатором чтения является лишь побочным эффектом неинтеграции. Вместо этого я бы использовал поле volatile или Thread.VolatileRead.
Ответ 3
При правильном коде оптимизация не должна менять свою семантику. Поэтому оптимизатор не может вносить ошибки, если ошибка уже не была в коде.