web-dev-qa-db-fra.com

Soulever le fil de l'événement en toute sécurité - meilleure pratique

Afin de déclencher un événement, nous utilisons une méthode OnEventName comme celle-ci:

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

Mais quelle est la différence avec celui-ci?

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

Apparemment, le premier est thread-safe, mais pourquoi et comment?

Il n'est pas nécessaire de démarrer un nouveau thread?

40
Jérémie Bertrand

Il y a une petite chance que SomethingHappened devienne null après la vérification nulle mais avant l'invocation. Cependant, MulticastDelagates sont immuables, donc si vous attribuez d'abord une variable, une vérification nulle par rapport à la variable et que vous l'invoquez, vous êtes à l'abri de ce scénario (auto-plug: j'ai écrit un blog à ce sujet il y a quelque temps).

Il y a cependant un revers de la médaille; si vous utilisez l'approche des variables temporaires, votre code est protégé contre NullReferenceExceptions, mais il se peut que l'événement appelle des écouteurs d'événement après qu'ils ont été détachés de l'événement . C'est juste quelque chose à traiter de la manière la plus gracieuse possible.

Afin de contourner cela, j'ai une méthode d'extension que j'utilise parfois:

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

En utilisant cette méthode, vous pouvez invoquer des événements comme celui-ci:

protected void OnSomeEvent(EventArgs e)
{
    SomeEvent.SafeInvoke(this, e);
}
53
Fredrik Mörk

Depuis C # 6.0, vous pouvez utiliser l'opérateur monadique Null-conditionnel ?. pour vérifier les événements null et raise de manière simple et sécurisée pour les threads.

SomethingHappened?.Invoke(this, args);

Il est thread-safe car il n'évalue le côté gauche qu'une seule fois et le conserve dans une variable temporaire. Vous pouvez lire plus ici en partie intitulé Opérateurs Null-conditionnels.

Mise à jour: En fait, la mise à jour 2 pour Visual Studio 2015 contient désormais une refactorisation pour simplifier les appels de délégués qui se retrouveront avec exactement ce type de notation. Vous pouvez en lire plus dans cette annonce .

35
Krzysztof Branicki

Je garde cet extrait de code comme référence pour un accès aux événements multithread sécurisé pour la définition et le déclenchement:

    /// <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);
        }
    }
14
Jesse C. Slicer

Pour .NET 4.5, il est préférable d'utiliser Volatile.Read pour affecter une variable temporaire.

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

Mise à jour:

C'est expliqué dans cet article: http://msdn.Microsoft.com/en-us/magazine/jj883956.aspx . En outre, cela a été expliqué dans la quatrième édition de "CLR via C #".

L'idée principale est que le compilateur JIT peut optimiser votre code et supprimer la variable temporaire locale. Donc, ce code:

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

sera compilé dans ceci:

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

Cela se produit dans certaines circonstances particulières, mais cela peut arriver.

12
rpeshkov

Déclarez votre événement comme ceci pour obtenir la sécurité des threads:

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

Et invoquez-le comme ceci:

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

Bien que la méthode ne soit plus nécessaire ..

7
jgauffin

Cela dépend de ce que vous entendez par thread-safe. Si votre définition inclut uniquement la prévention de NullReferenceException, le premier exemple est plus plus sûr. Cependant, si vous optez pour une définition plus stricte dans laquelle les gestionnaires d'événements doivent être appelés s'ils existent, alors ni est sûr . La raison tient à la complexité du modèle de mémoire et des barrières. Il se peut qu'il y ait, en fait, des gestionnaires d'événements chaînés au délégué, mais le thread lit toujours la référence comme nulle. La manière correcte de corriger les deux est de créer une barrière de mémoire explicite au point où la référence du délégué est capturée dans une variable locale. Il existe plusieurs façons de procéder.

  • Utilisez le mot clé lock (ou tout autre mécanisme de synchronisation).
  • Utilisez le mot clé volatile sur la variable d'événement.
  • Utilisation Thread.MemoryBarrier.

Malgré le problème délicat de délimitation qui vous empêche de faire l'initialisation d'une ligne, je préfère toujours la méthode lock.

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

Il est important de noter que dans ce cas spécifique, le problème de barrière de mémoire est probablement théorique car il est peu probable que les lectures de variables soient levées en dehors des appels de méthode. Mais, il n'y a aucune garantie, surtout si le compilateur décide d'inline la méthode.

7
Brian Gideon

En fait, le premier est thread-safe, mais pas le second. Le problème avec le second est que le délégué SomethingHappened pourrait être changé en null entre la vérification null et l'appel. Pour une explication plus complète, voir http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx .

3
Nicole Calinoiu

En fait, non, le deuxième exemple n'est pas considéré comme thread-safe. L'événement SomethingHappened peut être évalué à non nul dans le conditionnel, puis être nul lorsqu'il est invoqué. C'est une condition de course classique.

1
Curt Nichols

J'ai essayé de pimp out Jesse C. Slicer réponse avec:

  • Possibilité de se souscrire/se désinscrire de n'importe quel thread pendant une relance (condition de course supprimée)
  • Surcharge de l'opérateur pour + = et - = au niveau de la classe
  • Délégués définis par l'appelant générique

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

Usage:

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

Des problèmes majeurs avec cette approche?

Le code n'a été que brièvement testé et modifié un peu lors de l'insertion.
Pré-reconnaître que la Liste <> n'est pas un excellent choix si elle comporte de nombreux éléments.

1
crokusek

Pour que l'un d'eux soit thread-safe, vous supposez que tous les objets qui s'abonnent à l'événement sont également thread-safe.

0
Martin Brown