web-dev-qa-db-fra.com

Vérification de la valeur null avant la distribution de l'événement ... thread safe?

Quelque chose qui me trouble, mais qui n’a jamais posé de problème ... La manière recommandée d’envoyer un événement est la suivante:

public event EventHandler SomeEvent;
...
{
    ....
    if(SomeEvent!=null)SomeEvent();
}

Dans un environnement multithread, comment ce code garantit-il qu'un autre thread ne modifiera pas la liste d'appels de SomeEvent entre la vérification de la valeur null et l'appel de l'événement?

35
spender

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

SomeEvent?.Invoke(this, args);

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

22
Krzysztof Branicki

Comme vous l'avez fait remarquer, lorsque plusieurs threads peuvent accéder à SomeEvent simultanément, un thread peut vérifier si SomeEvent est null et déterminer que ce n'est pas le cas. Juste après cela, un autre thread pourrait supprimer le dernier délégué enregistré de SomeEvent. Lorsque le premier thread tente de lever SomeEvent, une exception sera levée. Un moyen raisonnable d'éviter ce scénario est: 

protected virtual void OnSomeEvent(EventArgs args) 
{
    EventHandler ev = SomeEvent;
    if (ev != null) ev(this, args);
}

Cela fonctionne car chaque fois qu'un délégué est ajouté ou supprimé d'un événement à l'aide des implémentations par défaut des accesseurs d'ajout et de suppression, les méthodes statiques Delegate.Combine et Delegate.Remove sont utilisées. Chacune de ces méthodes renvoie une nouvelle instance d'un délégué, plutôt que de modifier celle qui lui est transmise. 

De plus, l'affectation d'une référence à un objet dans .NET est atomic , et les implémentations par défaut des accesseurs d'événement d'ajout et de suppression sont synchronized . Ainsi, le code ci-dessus réussit en copiant d'abord le délégué multidiffusion de l'événement dans une variable temporaire. Toute modification apportée à SomeEvent après ce point n’affectera pas la copie que vous avez faite et stockée. Ainsi, vous pouvez maintenant vérifier si des délégués ont été enregistrés et les invoquer par la suite.

Notez que cette solution résout un problème de race, à savoir le fait qu'un gestionnaire d'événements est nul lorsqu'il est appelé. Il ne résout pas le problème de la disparition d'un gestionnaire d'événements lorsqu'il est appelé ou de l'abonnement d'un gestionnaire d'événements après la prise de la copie. 

Par exemple, si un gestionnaire d'événements dépend de l'état détruit dès qu'il n'est pas abonné, cette solution peut appeler un code qui ne peut pas s'exécuter correctement. Voir excellente entrée de blog d'Eric Lippert pour plus de détails. Voir aussi cette question StackOverflow et ses réponses .

EDIT: Si vous utilisez C # 6.0, alors la réponse de Krzysztof semble être une bonne solution.

55
RoadWarrior

Le moyen le plus simple de supprimer cette vérification NULL consiste à affecter le gestionnaire d'événements à un délégué anonyme. La pénalité encourue dans très peu et vous soulage de tous les contrôles nuls, conditions de course, etc.

public event EventHandler SomeEvent = delegate {};

Question connexe: Y a-t-il un inconvénient à ajouter un délégué vide anonyme à la déclaration d'événement?

21
Cherian

La manière recommandée est un peu différente et utilise un temporaire comme suit:

EventHandler tmpEvent = SomeEvent;
if (tmpEvent != null)
{
    tmpEvent();
}
4
denis phillips

Approche plus sûre:


public class Test
{
    private EventHandler myEvent;
    private object eventLock = new object();

    private void OnMyEvent()
    {
        EventHandler handler;

        lock(this.eventLock)
        {
            handler = this.myEvent;
        }
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    public event MyEvent
    {
        add
        {
            lock(this.eventLock)
            {
                this.myEvent += value;
            }
        }
        remove
        {
            lock(this.eventLock)
            {
                this.myEvent -= value;
            }
        }

    }
}

-facture

3

J'aimerais suggérer une légère amélioration à la réponse de RoadWarrior en utilisant En utilisant une fonction d'extention pour EventHandler:

public static class Extensions
{
    public static void Raise(this EventHandler e, object sender, EventArgs args = null)
    {
        var e1 = e;

        if (e1 != null)
        {
            if (args == null)
                args = new EventArgs();

            e1(sender, args);
        }                
    }
  }

Avec cette extension, les événements peuvent être générés simplement:

class SomeClass { événement public EventHandler MyEvent;

void SomeFunction()
{
    // code ...

    //---------------------------
    MyEvent.Raise(this);
    //---------------------------
}

}

c #événements

0
Gidi Baum