Безопасный подъем событий - лучшая практика

Чтобы поднять событие, мы используем метод OnEventName следующим образом:

protected virtual void OnSomethingHappened(EventArgs e) 
{
    EventHandler handler = SomethingHappened;
    if (handler != null) 
    {
        handler(this, e);
    }
}

Но в чем разница с этим?

protected virtual void OnSomethingHappened(EventArgs e) 
{
    if (SomethingHappened!= null) 
    {
        SomethingHappened(this, e);
    }
}

По-видимому, первый является потокобезопасным, но почему и как?

Не нужно начинать новый поток?

Ответ 1

Существует небольшая вероятность, что SomethingHappened станет null после нулевой проверки, но до вызова. Тем не менее, MulticastDelagate являются неизменяемыми, поэтому, если вы сначала назначили переменную, нулевую проверку против переменной и вызовите ее, вы можете быть в безопасности от этого сценария (self plug: я написал сообщение в блоге об этом некоторое время назад).

Однако есть оборотная сторона монеты; если вы используете подход temp variable, ваш код защищен от NullReferenceException s, но может случиться так, что событие вызовет прослушиватели событий после того, как они будут отделены от события. Это всего лишь то, с чем можно справиться самым изящным способом.

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

public static class EventHandlerExtensions
{
    public static void SafeInvoke<T>(this EventHandler<T> evt, object sender, T e) where T : EventArgs
    {
        if (evt != null)
        {
            evt(sender, e);
        }
    }
}

Используя этот метод, вы можете вызвать такие события, как это:

protected void OnSomeEvent(EventArgs e)
{
    SomeEvent.SafeInvoke(this, e);
}

Ответ 2

Начиная с С# 6.0 вы можете использовать монадический оператор Null-условный оператор ?. для проверки событий с нулевым и повышающим эффектом простым и потокобезопасным способом.

SomethingHappened?.Invoke(this, args);

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

Обновление: на самом деле обновление 2 для Visual Studio 2015 теперь содержит рефакторинг для упрощения делегирования делегатов, который в конечном итоге будет иметь именно этот тип обозначений. Вы можете прочитать об этом в этом объявлении.

Ответ 3

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

    /// <summary>
    /// Lock for SomeEvent delegate access.
    /// </summary>
    private readonly object someEventLock = new object();

    /// <summary>
    /// Delegate variable backing the SomeEvent event.
    /// </summary>
    private EventHandler<EventArgs> someEvent;

    /// <summary>
    /// Description for the event.
    /// </summary>
    public event EventHandler<EventArgs> SomeEvent
    {
        add
        {
            lock (this.someEventLock)
            {
                this.someEvent += value;
            }
        }

        remove
        {
            lock (this.someEventLock)
            {
                this.someEvent -= value;
            }
        }
    }

    /// <summary>
    /// Raises the OnSomeEvent event.
    /// </summary>
    public void RaiseEvent()
    {
        this.OnSomeEvent(EventArgs.Empty);
    }

    /// <summary>
    /// Raises the SomeEvent event.
    /// </summary>
    /// <param name="e">The event arguments.</param>
    protected virtual void OnSomeEvent(EventArgs e)
    {
        EventHandler<EventArgs> handler;

        lock (this.someEventLock)
        {
            handler = this.someEvent;
        }

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

Ответ 4

Для .NET 4.5 лучше использовать Volatile.Read для назначения временной переменной.

protected virtual void OnSomethingHappened(EventArgs e) 
{
    EventHandler handler = Volatile.Read(ref SomethingHappened);
    if (handler != null) 
    {
        handler(this, e);
    }
}

Update:

В этой статье объясняется: http://msdn.microsoft.com/en-us/magazine/jj883956.aspx. Кроме того, это было объяснено в четвертом издании "CLR via С#".

Основная идея заключается в том, что JIT-компилятор может оптимизировать ваш код и удалить локальную временную переменную. Итак, этот код:

protected virtual void OnSomethingHappened(EventArgs e) 
{
    EventHandler handler = SomethingHappened;
    if (handler != null) 
    {
        handler(this, e);
    }
}

будет скомпилирован следующим образом:

protected virtual void OnSomethingHappened(EventArgs e) 
{
    if (SomethingHappened != null) 
    {
        SomethingHappened(this, e);
    }
}

Это происходит в определенных особых обстоятельствах, однако это может произойти.

Ответ 5

Объявите свое событие, подобное этому, для обеспечения безопасности потоков:

public event EventHandler<MyEventArgs> SomethingHappened = delegate{};

И вызовите его вот так:

protected virtual void OnSomethingHappened(MyEventArgs e)   
{  
    SomethingHappened(this, e);
} 

Хотя этот метод больше не нужен.

Ответ 6

Это зависит от того, что вы подразумеваете под потокобезопасностью. Если ваше определение включает только предотвращение NullReferenceException, то первый пример более безопасен. Однако, если вы перейдете к более строгому определению, в котором обработчики событий должны быть вызваны, если они существуют, то ни не является безопасным. Причина связана с сложностями модели памяти и барьеров. Может быть, на самом деле есть обработчики событий, привязанные к делегату, но поток всегда читает ссылку как null. Правильный способ исправить это - создать явный барьер памяти в точке, где ссылка делегата будет записана в локальную переменную. Существует несколько способов сделать это.

  • Используйте ключевое слово lock (или любой механизм синхронизации).
  • Используйте ключевое слово volatile для переменной события.
  • Используйте Thread.MemoryBarrier.

Несмотря на неудобную задачу обзора, которая мешает вам выполнять однострочный инициализатор, я по-прежнему предпочитаю метод lock.

protected virtual void OnSomethingHappened(EventArgs e)           
{          
    EventHandler handler;
    lock (this)
    {
      handler = SomethingHappened;
    }
    if (handler != null)           
    {          
        handler(this, e);          
    }          
}          

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

Ответ 7

Собственно, первый является потокобезопасным, а второй - нет. Проблема со вторым заключается в том, что делегат SomethingHappened может быть изменен на null между нулевой верификацией и вызовом. Для более полного объяснения см. http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx.

Ответ 8

Собственно, нет, второй пример не считается потокобезопасным. Событие SomethingHappened может вычисляться с ненулевым значением в условном выражении, затем при вызове должно быть null. Это классическое состояние гонки.

Ответ 9

Я попытался вытащить Джесси Слайсер с помощью:

  • Возможность добавления/отмены подписки из любого потока во время рейза (состояние гонки удалено)
  • Перегрузка оператора для + = и - = на уровне класса
  • Общие делегаты, определенные делегатом

    public class ThreadSafeEventDispatcher<T> where T : class
    {
        readonly object _lock = new object();
    
        private class RemovableDelegate
        {
            public readonly T Delegate;
            public bool RemovedDuringRaise;
    
            public RemovableDelegate(T @delegate)
            {
                Delegate = @delegate;
            }
        };
    
        List<RemovableDelegate> _delegates = new List<RemovableDelegate>();
    
        Int32 _raisers;  // indicate whether the event is being raised
    
        // Raises the Event
        public void Raise(Func<T, bool> raiser)
        {
            try
            {
                List<RemovableDelegate> raisingDelegates;
                lock (_lock)
                {
                    raisingDelegates = new List<RemovableDelegate>(_delegates);
                    _raisers++;
                }
    
                foreach (RemovableDelegate d in raisingDelegates)
                {
                    lock (_lock)
                        if (d.RemovedDuringRaise)
                            continue;
    
                    raiser(d.Delegate);  // Could use return value here to stop.                    
                }
            }
            finally
            {
                lock (_lock)
                    _raisers--;
            }
        }
    
        // Override + so that += works like events.
        // Adds are not recognized for any event currently being raised.
        //
        public static ThreadSafeEventDispatcher<T> operator +(ThreadSafeEventDispatcher<T> tsd, T @delegate)
        {
            lock (tsd._lock)
                if (!tsd._delegates.Any(d => d.Delegate == @delegate))
                    tsd._delegates.Add(new RemovableDelegate(@delegate));
            return tsd;
        }
    
        // Override - so that -= works like events.  
        // Removes are recongized immediately, even for any event current being raised.
        //
        public static ThreadSafeEventDispatcher<T> operator -(ThreadSafeEventDispatcher<T> tsd, T @delegate)
        {
            lock (tsd._lock)
            {
                int index = tsd._delegates
                    .FindIndex(h => h.Delegate == @delegate);
    
                if (index >= 0)
                {
                    if (tsd._raisers > 0)
                        tsd._delegates[index].RemovedDuringRaise = true; // let raiser know its gone
    
                    tsd._delegates.RemoveAt(index); // okay to remove, raiser has a list copy
                }
            }
    
            return tsd;
        }
    }
    

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

    class SomeClass
    {   
        // Define an event including signature
        public ThreadSafeEventDispatcher<Func<SomeClass, bool>> OnSomeEvent = 
                new ThreadSafeEventDispatcher<Func<SomeClass, bool>>();

        void SomeMethod() 
        {
            OnSomeEvent += HandleEvent; // subscribe

            OnSomeEvent.Raise(e => e(this)); // raise
        }

        public bool HandleEvent(SomeClass someClass) 
        { 
            return true; 
        }           
    }

Любые серьезные проблемы с этим подходом?

Код был только кратко протестирован и немного отредактирован для вставки.
Подтвердите, что List < > не отличный выбор, если много элементов.

Ответ 10

Для того, чтобы любой из них был потокобезопасным, вы предполагаете, что все объекты, которые подписываются на событие, также являются потокобезопасными.