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

Чтобы вызвать событие, мы используем метод 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);
    }
}

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

Нет необходимости начинать новую тему?

10 ответов

Решение

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

Есть обратная сторона медали, хотя; если вы используете временную переменную, ваш код защищен от 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);
}

Начиная с C# 6.0 вы можете использовать монадический нуль-условный оператор ?. проверять нулевое значение и генерировать события простым и поточно-ориентированным способом.

SomethingHappened?.Invoke(this, args);

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

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

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

    /// <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);
        }
    }

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

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

Обновить:

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

Основная идея заключается в том, что 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);
    }
}

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

Объявите ваше событие следующим образом, чтобы получить безопасность потока:

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

И вызвать это так:

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

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

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

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

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

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

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

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

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

Я попытался выяснить ответ Jesse C. Slicer:

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

    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<> не лучший выбор, если много элементов.

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

Другие вопросы по тегам