Wednesday, May 19, 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);