Вызов события и многопоточность
От: valker  
Дата: 05.10.09 07:51
Оценка:
Здравствуйте!

Возник вопрос: как правильно реализовывать вызов события (event raising)? Я так понимаю, что сам смысл "правильности" зависит от того, где мы вызываем событие. Если приложение однопоточное, то, очевидно достаточно формы InvokeEvent1. А вот с многопоточными приложениями не всё понятно.
Какой метод реализации выбрать? Есть ли рекомендации на этот счёт?

Спасибо.

public class Test
{
    public event EventHandler Event;

    private void InvokeEvent1(EventArgs e)
    {
        if (Event != null)
        {
            Event(this, e);
        }
    }
    private void InvokeEvent2(EventArgs e)
    {
        EventHandler handler = Event;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    private void InvokeEvent3(EventArgs e)
    {
        EventHandler handler;
        lock (this)
        {
            handler = Event; 
        }

        if (handler != null)
        {
            handler(this, e);
        }
    }
}

public class Test2
{
    private object _sync = new object();

    private EventHandler _event;

    public event EventHandler Event
    {
        add
        {
            lock (_sync)
            {
                _event += value; 
            }
        }
        remove
        {
            lock (_sync)
            {
                _event -= value; 
            }
        }
    }

    private void InvokeEvent(EventArgs e)
    {
        EventHandler handler;
        lock (_sync)
        {
            handler = _event;
        }

        if (handler != null)
        {
            handler(this, e);
        }
    }
}
Re: Вызов события и многопоточность
От: TK Лес кывт.рф
Дата: 05.10.09 08:05
Оценка: 4 (1) +3
Здравствуйте, valker, Вы писали:

V>Здравствуйте!


V>Возник вопрос: как правильно реализовывать вызов события (event raising)? Я так понимаю, что сам смысл "правильности" зависит от того, где мы вызываем событие. Если приложение однопоточное, то, очевидно достаточно формы InvokeEvent1. А вот с многопоточными приложениями не всё понятно.

V>Какой метод реализации выбрать? Есть ли рекомендации на этот счёт?

Экземпляры System.Delegate являются "неизменяемыми" поэтому, специальной синхронизации для них не требуется. Достаточно скопировать экзепляр, проверить копию на null и уже через нее сделать вызов.

Для подписки/отписки (реализация add/remove руками) синхронизация нужна. Стандартная реализация использует атрибут [MethodImpl(MethodImplOptions.Synchronized)] что, аналогично использованию lock (this) { ... }
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re: Вызов события и многопоточность
От: IB Австрия http://rsdn.ru
Дата: 05.10.09 08:37
Оценка:
Здравствуйте, valker, Вы писали:

V>Возник вопрос: как правильно реализовывать вызов события (event raising)?

По этому поводу есть отличная статья Эрика Липперта: http://blogs.msdn.com/ericlippert/archive/2009/04/29/events-and-races.aspx

P. S.
лишнее подтверждение того, что вводя события напрямую в язык, несколько поторопились...
... << RSDN@Home 1.2.0 alpha 4 rev. 1082>>
Мы уже победили, просто это еще не так заметно...
Re[2]: Вызов события и многопоточность
От: valker  
Дата: 05.10.09 08:38
Оценка:
Здравствуйте, TK, Вы писали:

TK>Здравствуйте, valker, Вы писали:


V>>Здравствуйте!


V>>Возник вопрос: как правильно реализовывать вызов события (event raising)? Я так понимаю, что сам смысл "правильности" зависит от того, где мы вызываем событие. Если приложение однопоточное, то, очевидно достаточно формы InvokeEvent1. А вот с многопоточными приложениями не всё понятно.

V>>Какой метод реализации выбрать? Есть ли рекомендации на этот счёт?

TK>Экземпляры System.Delegate являются "неизменяемыми" поэтому, специальной синхронизации для них не требуется. Достаточно скопировать экзепляр, проверить копию на null и уже через нее сделать вызов.


TK>Для подписки/отписки (реализация add/remove руками) синхронизация нужна. Стандартная реализация использует атрибут [MethodImpl(MethodImplOptions.Synchronized)] что, аналогично использованию lock (this) { ... }


Спасибо. Но я слышал вот здесь что:

Programmers without a strong background in multithreaded programming may not immediately detect why this solution is wrong. Delegates are immutable reference types, so the local variable copy is atomic; no problem there. The problem exists in the memory model: it is possible that an out-of-date value for the delegate field is held in one processor's cache. Without going into a painful level of detail, in order to ensure that one is reading the current value of a non-volatile field, one must either issue a memory barrier or wrap the copy operation within a lock (and it must be the same lock acquired by the event add/remove methods).


Или же нам нужно "закладываться" на то, что подписчик может быть вызван после операции отписки?

Заранее спасибо.
Re: Вызов события и многопоточность
От: _FRED_ Черногория
Дата: 05.10.09 08:45
Оценка:
Здравствуйте, valker, Вы писали:

V>Возник вопрос: как правильно реализовывать вызов события (event raising)? Я так понимаю, что сам смысл "правильности" зависит от того, где мы вызываем событие. Если приложение однопоточное, то, очевидно достаточно формы InvokeEvent1. А вот с многопоточными приложениями не всё понятно.

V>Какой метод реализации выбрать? Есть ли рекомендации на этот счёт?

Дополню ответ
Автор: TK
Дата: 05.10.09
TK.

In general, avoid locking on a public type, or instances beyond your code's control. The common constructs lock (this), lock (typeof (MyType)), and lock ("myLock") violate this guideline:

  • lock (this) is a problem if the instance can be accessed publicly.

  • здесь

    Поэтому для поддержки событий в потокобезопасном классе field-like event-ы годятся плохо. Приходится делать так:
    public class Test2
    {
        private readonly object _sync = new object(); // (!) http://rsdn.ru/forum/dotnet/2508474.aspx
    
        private EventHandler _event;
    
        public event EventHandler Event
        {
            add
            {
                lock (_sync)
                {
                    _event += value; 
                }
            }
            remove
            {
                lock (_sync)
                {
                    _event -= value; 
                }
            }
        }
    
        private void InvokeEvent(EventArgs e)
        {
            EventHandler handler = _event;
            if (handler != null)
            {
                handler(this, e);
            }
        }
    }

    Если же подобных событий у класса несколько мне больше нравится подход с объявлением словаря подписчиков и хранения делегатов в нём, нежели использование многих [или даже нескольких] полей. Стандартным словарём является EventHandlerList. Примеры работы с ним описаны в статье How to: Handle Multiple Events Using Event Properties. Рихтер в одной из своих книг под .NET приводит свой класс на заданную тему EventHandlerSet. Сейчас вот удалось вот такую реализацию: A Truly Thread-Safe Event Handler Collection.

    Самое удобное в подходе со словарями заключается в том, что поддержка многопоточности "спрятана" внутри словаря и указывается один раз при инициализации словаря. При всей остальной работе — в коде подписки\отписки\вызова заботится о синхронизации не нужно.
    Help will always be given at Hogwarts to those who ask for it.
    Re[3]: Вызов события и многопоточность
    От: TK Лес кывт.рф
    Дата: 05.10.09 08:49
    Оценка: 1 (1) +2
    Здравствуйте, valker, Вы писали:

    V>Или же нам нужно "закладываться" на то, что подписчик может быть вызван после операции отписки?


    Если подписчик подписывается на сообщения которые, генерируются "асинхронно" то, простой вариант — требовать, что подписчик должен игнорировать события пришедшие после отписки. Введение синхронизации в "генерацию" событий принесет больше проблем, чем потенциальной выгоды.
    Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
    Re[3]: Вызов события и многопоточность
    От: drol  
    Дата: 05.10.09 22:14
    Оценка: +1
    Здравствуйте, valker, Вы писали:

    V>Спасибо. Но я слышал вот здесь что:


    ИМХО, у автора текста по ссылке некоторая каша в голове, хотя ключевые слова указаны правильные. Касательно же процитированного:

    1) Кэш тут совершенно не при делах. Бо на стандартных архитектурах x86/x64 как минимум кэш данных когерентен на уровне железа, включая и мультисокетные конфигурации.

    2) Проблема которую пытается описать автор не имеет никакого отношения к event'ам как таковым. Это проблема конкретного подписчика event'а.

    V>Или же нам нужно "закладываться" на то, что подписчик может быть вызван после операции отписки?


    Всё аналогично стандартным правилам для обычного многопоточного кода.

    Если у нескольких потоков есть некоторое изменяемое общее (shared) состояние, то для работы с этим состоянием необходимо использовать какой-то из механизов синхронизации.

    В нашем случае: есть поток исполняющий код подписчика, и есть поток модифицирующий event. Если подписчик использует некоторое изменяемое состояние связанное с содержимым event'а, то 100% надо думать о синхронизации. Если же такого состояния нет, то какая подписчику разница отписали его к моменту начала исполнения, или ещё не успели ???
     
    Подождите ...
    Wait...
    Пока на собственное сообщение не было ответов, его можно удалить.