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

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

50 голосов
/ 08 сентября 2010

Существует небольшая вероятность того, что SomethingHappened станет null после нулевой проверки, но до вызова. Тем не менее, MulticastDelagate s являются неизменяемыми, поэтому, если вы сначала назначите переменную, проверите нулевую проверку по переменной и вызовете ее, вы в безопасности от этого сценария (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);
}
33 голосов
/ 06 сентября 2015

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

SomethingHappened?.Invoke(this, args);

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

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

13 голосов
/ 08 сентября 2010

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

    /// <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);
        }
    }
11 голосов
/ 18 июня 2013

Для .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);
    }
}

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

7 голосов
/ 08 сентября 2010

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

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

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

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

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

6 голосов
/ 08 сентября 2010

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

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

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

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

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

3 голосов
/ 08 сентября 2010

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

1 голос
/ 22 октября 2013

Я пытался вытолкнуть Джесси С. Слайсер ответ с:

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

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

1 голос
/ 08 сентября 2010

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

0 голосов
/ 08 сентября 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...