Может ли оптимизация встраивания метода вызвать условия гонки? - PullRequest
15 голосов
/ 24 февраля 2010

Как видно из этого вопроса: Возбуждение событий C # с помощью метода расширения - это плохо?

Я думаю об использовании этого метода расширения, чтобы безопасно вызвать событие:

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);
}

Ответы [ 3 ]

7 голосов
/ 24 февраля 2010

Проблема не заключалась бы в том, чтобы встроить метод - это было бы, если бы 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 выполняет inline SafeRaise, он будет встроен в этот фрагмент - потому что встроенный параметр заканчивается как новая локальная переменная, эффективно. Проблема была бы только в том случае, если JIT неправильно указывал на это, сохраняя два отдельных чтения myEvent.

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

5 голосов
/ 02 марта 2010

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

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

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

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

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

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

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

1 голос
/ 24 февраля 2010

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

...