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.
Ссылки:
- C# Events and Thread Safety
- Checking for null before event dispatching...thread safe?
- Events and Races
Самый лучший совет про события - не использовать их.
ОтветитьУдалитьЯ бы сказал, использовать их с умом там, где они нужны. События естественны в приложении, иногда без них никуда. Например, если Ваш объект мониторит другой (слушает сетевой порт, следит за файловой системой, или просто получает уведомление об изменении состояния объекта)
ОтветитьУдалитьОчень важно понимать, что здесь:
ОтветитьУдалить// Очень важно, сделать копию события:
EventHandler handler = SomeEvent;
делается не "копия события", а всего лишь запоминается делегат.
> Самый лучший совет про события - не использовать их.
ОтветитьУдалитьНе надо философских изречений. То же самое можно
сказать про большую часть конструкций любого языка. Конкатенацию строк тоже не использовать? В общем, факты в студию.
> делается не "копия события", а всего лишь запоминается делегат.
Еще точнее, делается копия экземпляра делегата, т.е. списка вызова (что видимо и подразумевалось под "запоминается делегат").
Статья не покрывает потокобезопасность подписок и отписок. А именно: как они защищены по умолчанию ([MethodImpl(MethodImplOptions.Synchronized)] начиная с .NET 2.0) и как это поведение можно изменить.
@till
ОтветитьУдалитьА можно линк?
Я ориентируюсь на Рихтера, он в своей книге (CLR via C# 3th) приводит 3 возможных версии thread-safe add/remove. Я рассмотрел наиболее популярный вариант.
@ttil: Да нет же, "копия… списка вызовов" в отквоченной мной строке тем более не делается. Делается копия _ссылки_ на "список вызовов".
ОтветитьУдалить@_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 там есть серьезные нюансы. Как это сделано, какие могут быть проблемы, решения и побочные эффекты от решений.
Кстати, в описанном решении тоже есть нюанс - вызов метода, который отписался.
Спасибо за ссылки и комментарий.
ОтветитьУдалитьХотя Рихтер рекомендует использовать в качестве "копирования" следующее:
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 :)
@ttil
ОтветитьУдалитьСобытия позволяют писать код где ни попадя. Тем самым с легкостью нарушая SRP, что влечет за собой трудность рефакторинга. Не зря в .NET 4 ввели классы IObserver и IObservable.