Feel Good.

14 мая 2010

Thread-safe events

Если Вы пишите многопоточное приложение, то всегда стоит задумываться о потоко-безопасности (thread safe). Сегодня затронем важную тему про то, как правильно возбуждать событие (event raise), доступ к которому осуществляется в нескольких потоках.

class SomeClass

{

    // Доступ к данному событию происходит

    // в нескольких потоках.

    public event EventHandler SomeEvent;

}


Стандартный, потоко-безопасный (thread safe) шаблон вызова события выглядит следующим образом:

// Потоко-безопасный вызов.

public void OnSomeEventThreadSafe(EventArgs args)

{

    // Очень важно, сделать копию события:

    EventHandler handler = SomeEvent;

 

    // И далее работать только с ней:

    if (handler != null)

    {

        handler(this, args);

    }           

}



Зачем, нам работать с копией, если мы можем и с оригиналом? Если коротко, то при многопоточном доступе к событию SomeEvent, у нас может возникнуть исключение. Например, если второй поток удалит последний зарегистрированный delegate из SomeEvent, в то время как в первом потоке проверка SomeEvent!=null была успешно пройдена.

// Не потоко-безопасный вызов.

void OnSomeEvent(EventArgs args)

{

    if (SomeEvent != null)

    {

        // SomeEvent - может оказаться равным null.

        SomeEvent(this, args);

    }

}


Видно, что при добавлении нового события, при потоко-безопасном подходе приходится дублировать один и тот же кусок кода (копирование-проверка-вызов). Данное дублирование можно избежать, если вынести всю логику в метод-расширение (Extension Methods):

static class EventHandlerExt

{

    // Метод расширение, для организации потоко-безопасного вызова события.

    public static void Raise(this EventHandler self, object sender, EventArgs args)

    {

        EventHandler handler = self;

        if (handler != null)

        {

            handler(sender, args);

        }

    }

}


Использование:

SomeEvent.Raise(this, EventArgs.Empty);



UPD:
Хотя Рихтер рекомендует использовать в качестве "копирования" следующее:

EventHandler temp = Thread.VolatileRead(ref SomeEvent);

//или

EventHandler temp = Interlocked.CompareExchange(ref SomeEvent, null, null);


объясняя это следующим:
Remember that delegates are immutable and this is why this technique works in theory. However, what a lot of developers don’t realize is that this code could be optimized by the compiler to remove the local temp variable entirely.... so a NullReferenceException is still possible.


Ссылки:
  1. C# Events and Thread Safety
  2. Checking for null before event dispatching...thread safe?
  3. Events and Races

Progg it

9 комментариев:

  1. Самый лучший совет про события - не использовать их.

    ОтветитьУдалить
  2. Я бы сказал, использовать их с умом там, где они нужны. События естественны в приложении, иногда без них никуда. Например, если Ваш объект мониторит другой (слушает сетевой порт, следит за файловой системой, или просто получает уведомление об изменении состояния объекта)

    ОтветитьУдалить
  3. Очень важно понимать, что здесь:

    // Очень важно, сделать копию события:
    EventHandler handler = SomeEvent;

    делается не "копия события", а всего лишь запоминается делегат.

    ОтветитьУдалить
  4. > Самый лучший совет про события - не использовать их.
    Не надо философских изречений. То же самое можно
    сказать про большую часть конструкций любого языка. Конкатенацию строк тоже не использовать? В общем, факты в студию.

    > делается не "копия события", а всего лишь запоминается делегат.
    Еще точнее, делается копия экземпляра делегата, т.е. списка вызова (что видимо и подразумевалось под "запоминается делегат").

    Статья не покрывает потокобезопасность подписок и отписок. А именно: как они защищены по умолчанию ([MethodImpl(MethodImplOptions.Synchronized)] начиная с .NET 2.0) и как это поведение можно изменить.

    ОтветитьУдалить
  5. @till
    А можно линк?

    Я ориентируюсь на Рихтера, он в своей книге (CLR via C# 3th) приводит 3 возможных версии thread-safe add/remove. Я рассмотрел наиболее популярный вариант.

    ОтветитьУдалить
  6. @ttil: Да нет же, "копия… списка вызовов" в отквоченной мной строке тем более не делается. Делается копия _ссылки_ на "список вызовов".

    ОтветитьУдалить
  7. @_FRED_
    Ммм... Видимо я не совсем правильно понял замечание, попробую еще раз понять :). Вы имели ввиду, что тут список вызовов будет один, но если на событие подпишутся или отпишутся, тогда для него создастся другой список. Так? Если так, то соглашусь :).

    @Илья Дубаденко
    Ага, вы уже читали самую актуальную информацию :) А я еще не знал, что в .NET 4 в этом месте немного улучшили (работаю с 3.5):
    http://marcgravell.blogspot.com/2010/03/revisited-fun-with-field-like-events.html

    Вот ссылочка, неплохо написано про как было в .NET 2 - 3.5:
    http://www.codeproject.com/Articles/37474/Threadsafe-Events.aspx
    (была там же статья, которая мне больше нравилась, но сейчас не попалась)

    Вы все правильно написали, просто полезно знать, как с потокобезопасностью для подписок/отписок, тем более, что в .NET младше 4 там есть серьезные нюансы. Как это сделано, какие могут быть проблемы, решения и побочные эффекты от решений.
    Кстати, в описанном решении тоже есть нюанс - вызов метода, который отписался.

    ОтветитьУдалить
  8. Спасибо за ссылки и комментарий.

    Хотя Рихтер рекомендует использовать в качестве "копирования" следующее:

    EventHandler temp = Thread.VolatileRead(ref SomeEvent);
    либо
    EventHandler temp =
    Interlocked.CompareExchange(ref SomeEvent, null, null);

    объясняя это следующим:
    Remember that delegates are immutable and this is why this technique works in theory. However, what a lot of developers don’t realize is that this code could be optimized by the compiler to remove the local temp variable entirely.... so a NullReferenceException is still possible.
    если коротко, то optimized by the compiler :)

    ОтветитьУдалить
  9. @ttil
    События позволяют писать код где ни попадя. Тем самым с легкостью нарушая SRP, что влечет за собой трудность рефакторинга. Не зря в .NET 4 ввели классы IObserver и IObservable.

    ОтветитьУдалить