49 votes

Soulever le fil de l'événement en toute sécurité - meilleures pratiques

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

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 d'ouvrir un nouveau fil de discussion ?

57voto

Fredrik Mörk Points 85694

Il y a une petite chance que SomethingHappened devient null après la vérification de la nullité mais avant l'invocation. Cependant, MulticastDelagate sont immuables, donc si vous assignez d'abord une variable, que vous vérifiez que la variable est nulle et que vous l'invoquez, vous êtes à l'abri de ce scénario (self plug : j'ai écrit une article de 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 les risques suivants NullReferenceException mais il se peut que l'événement invoque des récepteurs d'événements. après qu'ils aient été détachés de l'événement . Il s'agit simplement d'une situation à laquelle il faut faire face de la manière la plus élégante possible.

Pour contourner ce problème, 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 les événements comme ceci :

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

0 votes

Merci pour votre excellente réponse (et votre article de blog).

1 votes

J'ai aussi cette méthode d'extension dans ma bibliothèque principale avec exactement le même nom, qui fait le même travail exactement de la même manière ! Le nom de mon paramètre est cependant eventHandler.

0 votes

-1 : There is a back side of the coin though; if you use the temp variable approach (...) it could be that the event will invoke event listeners after they have been detached from the event : c'est toujours une possibilité ; elle est inévitable.

15voto

Jesse C. Slicer Points 11750

Je garde cet extrait comme référence pour un accès sûr et multithread aux événements, que ce soit pour la configuration ou 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);
        }
    }

2 votes

Oui, vous et moi sommes sur la même longueur d'onde. La réponse acceptée présente un problème subtil de barrière de mémoire que notre solution résout. En utilisant des add y remove est probablement inutile puisque le compilateur émet les verrous dans les implémentations automatiques. Cependant, je crois me souvenir que quelque chose a changé à ce sujet dans .NET 4.0.

0 votes

@Brian - d'accord, bien qu'avant la version 4.0, les verrous sont sur le système d'exploitation. this ce qui signifie que le code externe à la classe peut finir par perturber le mécanisme en verrouillant une instance. Jon Skeet a fourni l'inspiration ici csharpindepth.com/Articles/Chapitre2/Events.aspx#threading .

0 votes

Excellent lien. Et oui, je reconnais toutes les nuances avec le verrouillage sur this . Quelqu'un a-t-il un lien rapide pour les changements dans .NET 4.0 ? Sinon, je vais simplement consulter les spécifications.

13voto

black_wizard Points 1374

Pour .NET 4.5 il est préférable d'utiliser Volatile.Read pour assigner 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 . Il a également é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 locale temporaire. Donc ce code :

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

seront compilés dans ce document :

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

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

0 votes

Je ne connaissais pas l'utilité de Volatile pour ça. Pouvez-vous expliquer un peu pourquoi c'est mieux ?

0 votes

J'ai ajouté une explication à ma réponse.

0 votes

Encore une fois, cela ne protège que de l'exception NullReferenceException, cela n'a rien à voir avec le problème plus sérieux de la condition de course, où vous invoquez les gestionnaires d'événements qui viennent de se désinscrire.

7voto

jgauffin Points 51913

Déclarez votre événement comme ceci pour obtenir la sécurité du fil :

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

Et l'invoquer comme ceci :

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

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

Mise à jour 2021-09-01

Aujourd'hui, je me contenterais de faire (ce qui ne nécessite pas le délégué emty) :

SomethingHappened?.Invoke(e);

Quelqu'un a fait remarquer que l'utilisation d'un délégué vide a une plus grande surcharge. C'est vrai. Mais du point de vue de l'application, l'impact sur les performances est minime. Par conséquent, il est beaucoup plus important de choisir la solution qui a le code le plus propre.

1 votes

Cela peut causer des problèmes de performance si vous avez beaucoup d'événements à déclencher, puisque le délégué vide sera exécuté à chaque fois, qu'il y ait ou non des abonnés légitimes à l'événement. Il s'agit d'une petite surcharge, mais qui peut théoriquement s'accumuler.

9 votes

C'est un très petits frais généraux. Il y a des problèmes plus importants dans votre code qui devraient être optimisés en premier.

0 votes

Cela permet de se prémunir contre les erreurs de référence nulle, mais n'est pas strictement threadsafe, voir la discussion. stackoverflow.com/questions/786383/

7voto

Brian Gideon Points 26683

Cela dépend de ce que vous entendez par "thread-safe". Si votre définition ne comprend que la prévention de la NullReferenceException alors le premier exemple est plus sûr. Toutefois, si vous adoptez une définition plus stricte dans laquelle les gestionnaires d'événements doit être invoqués s'ils existent alors ni est sûr. La raison est liée à 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 enchaînés au délégué, mais que le thread lise toujours la référence comme nulle. La bonne façon de résoudre ces deux problèmes est de créer une barrière mémoire explicite au moment où la référence du délégué est capturée dans une variable locale. Il y a plusieurs façons de le faire.

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

Malgré l'embarras du problème de scoping qui vous empêche de faire l'initialisation en une ligne, je préfère toujours l'option lock méthode.

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 précis, le problème de la barrière mémoire est probablement sans objet car il est peu probable que la lecture des variables soit levée en dehors des appels de méthode. Mais il n'y a pas de garantie, surtout si le compilateur décide de mettre la méthode en ligne.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X