Метод, встраивающий оптимизацию, может вызвать условия состязания?

Как замечено в этом вопросе: события 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);
}

15
задан Community 23 May 2017 в 12:00
поделиться

3 ответа

Проблема заключалась бы не во встраивании метода - это был бы 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 .

Теперь, что касается того, почему вы только видели, как это произошло в режиме отладки: я подозреваю, что с подключенным отладчиком гораздо больше места для потоков, чтобы прерывать друг друга. Возможно, произошла какая-то другая оптимизация - но она не привела к поломке , так что ничего страшного.

7
ответ дан 1 December 2019 в 04:40
поделиться

При правильном коде оптимизации не должны изменять его семантику. Следовательно, оптимизатор не может внести ошибку, если ошибки уже не было в коде.

1
ответ дан 1 December 2019 в 04:40
поделиться

Это проблема модели памяти.

В основном вопрос заключается в следующем: если мой код содержит только одно логическое чтение, может ли оптимизатор ввести другое чтение?

Удивительно, но ответ таков: возможно

В спецификации CLR ничто не мешает оптимизаторам выполнять это. Оптимизация не нарушает однопоточную семантику, и шаблоны доступа к памяти гарантированно сохраняются только для изменчивых полей (и даже это упрощение, которое не на 100% верно).

Таким образом, независимо от того, используете ли вы локальную переменную или параметр, код не является поточно-ориентированным .

Однако платформа Microsoft .NET документирует другую модель памяти. В этой модели оптимизатору не разрешено вводить чтения, и ваш код безопасен (независимо от оптимизации встраивания).

Тем не менее, использование [MethodImplOptions] кажется странным взломом, поскольку предотвращение ввода оптимизатором операций чтения является лишь побочным эффектом отказа от встраивания. Вместо этого я бы использовал изменчивое поле или Thread.VolatileRead.

5
ответ дан 1 December 2019 в 04:40
поделиться
Другие вопросы по тегам:

Похожие вопросы: