Database
 sql >> Base de données >  >> RDS >> Database

Événements et threads dans .NET

Je voudrais vous dire tout de suite que cet article ne concernera pas les threads en particulier, mais les événements dans le contexte des threads dans .NET. Donc, je n'essaierai pas d'organiser correctement les discussions (avec tous les blocages, rappels, annulations, etc.). Il existe de nombreux articles sur ce sujet.

Tous les exemples sont écrits en C# pour la version 4.0 du framework (en 4.6, tout est un peu plus facile, mais il y a quand même beaucoup de projets en 4.0). Je vais également essayer de m'en tenir à la version 5.0 de C#.

Tout d'abord, je voudrais noter qu'il existe des délégués prêts pour le système d'événements .Net que je recommande fortement d'utiliser au lieu d'inventer quelque chose de nouveau. Par exemple, j'ai fréquemment été confronté aux 2 méthodes suivantes pour organiser des événements.

Première méthode :

 class WrongRaiser { public event Action MyEvent ; événement public Action MyEvent2 ; } 

Je recommanderais d'utiliser cette méthode avec précaution. Si vous ne l'universalisez pas, vous risquez d'écrire plus de code que prévu. En tant que tel, il ne définira pas une structure plus précise par rapport aux méthodes ci-dessous.

D'après mon expérience, je peux dire que je l'ai utilisé lorsque j'ai commencé à travailler avec des événements et que, par conséquent, je me suis ridiculisé. Maintenant, je n'y arriverais jamais.

Deuxième méthode :

 class WrongRaiser { événement public MyDelegate MyEvent ; } class MyEventArgs { public object SomeProperty { get; Positionner; } } délégué void MyDelegate(object sender, MyEventArgs e);

Cette méthode est tout à fait valable, mais elle convient aux cas spécifiques où la méthode ci-dessous ne fonctionne pas pour certaines raisons. Sinon, vous risquez d'avoir beaucoup de travail monotone.

Et maintenant, regardons ce qui a déjà été créé pour les événements.

Méthode universelle :

 class Raiser { public event EventHandler MyEvent ; } class MyEventArgs :EventArgs { public object SomeProperty { get; Positionner; } }

Comme vous pouvez le voir, nous utilisons ici la classe universelle EventHandler. Autrement dit, il n'est pas nécessaire de définir votre propre gestionnaire.

Les autres exemples présentent la méthode universelle.

Examinons l'exemple le plus simple du générateur d'événements.

 classe EventRaiser { int _counter ; événement public EventHandler CounterChanged ; public int Counter { get { return _counter ; } set { if (_counter !=valeur) { var old =_counter; _counter =valeur ; OnCounterChanged(ancien, valeur); } } } public void DoWork() { new Thread(new ThreadStart(() => { for (var i =0; i <10; i++) Counter =i; })).Start(); } void OnCounterChanged(int oldValue, int newValue) { if (CounterChanged !=null) CounterChanged.Invoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); } } class EventRaiserCounterChangedEventArgs :EventArgs { public int NewValue { get; Positionner; } public int AncienneValeur { obtenir ; Positionner; } public EventRaiserCounterChangedEventArgs(int oldValue, int newValue) { NewValue =newValue; AncienneValeur =ancienneValeur ; } }

Ici, nous avons une classe avec la propriété Counter qui peut être changée de 0 à 10. À cela, la logique qui change Counter est traitée dans un thread séparé.

Et voici notre point d'entrée :

    class Program
    {
        static void Main(string[] args)
        {
            var raiser = new EventRaiser();
            raiser.CounterChanged += Raiser_CounterChanged;
            raiser.DoWork();
            Console.ReadLine();
        }

        static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e)
        {
            Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue));
        }
    } 

Autrement dit, nous créons une instance de notre générateur, souscrivons au changement de compteur et, dans le gestionnaire d'événements, sortons les valeurs sur la console.

Voici ce que nous obtenons :

Jusqu'ici tout va bien. Mais réfléchissons, dans quel thread le gestionnaire d'événements est-il exécuté ?

La plupart de mes collègues ont répondu à cette question "En général un". Cela signifiait qu'aucun d'entre eux ne comprenait pas comment les délégués sont disposés. Je vais essayer de l'expliquer.

La classe Delegate contient des informations sur une méthode.

Il y a aussi son descendant, MulticastDelegate, qui a plus d'un élément.

Ainsi, lorsque vous vous abonnez à un événement, une instance du descendant MulticastDelegate est créée. Chaque abonné suivant ajoute une nouvelle méthode (gestionnaire d'événements) dans l'instance déjà créée de MulticastDelegate.

Lorsque vous appelez la méthode Invoke, les gestionnaires de tous les abonnés sont appelés un par un pour votre événement. À cela, le thread dans lequel vous appelez ces gestionnaires ne sait rien du thread dans lequel ils ont été spécifiés et, en conséquence, il ne peut rien insérer dans ce thread.

En général, les gestionnaires d'événements de l'exemple ci-dessus sont exécutés dans le thread généré dans la méthode DoWork(). Autrement dit, lors de la génération d'un événement, le thread qui l'a généré de cette manière attend l'exécution de tous les gestionnaires. Je vais vous montrer cela sans retirer les fils d'identification. Pour cela, j'ai changé quelques lignes de code dans l'exemple ci-dessus.

Preuve que tous les gestionnaires de l'exemple ci-dessus sont exécutés dans le thread qui a appelé l'événement

Méthode de génération de l'événement

 void OnCounterChanged(int oldValue, int newValue) { if (CounterChanged !=null) { CounterChanged.Invoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); Console.WriteLine(string.Format("Event Raiser :old ={0}, new ={1}", oldValue, newValue)); } }

Gestionnaire

 static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e) { Console.WriteLine(string.Format("OldValue :{0} ; NewValue :{1}", e.OldValue, e.NewValue) ); Thread.Sleep(500); } 

Dans le gestionnaire, nous envoyons le thread actuel en veille pendant une demi-seconde. Si les gestionnaires fonctionnaient dans le thread principal, ce temps serait suffisant pour qu'un thread généré dans DoWork() termine son travail et affiche ses résultats.

Cependant, voici ce que nous voyons vraiment :

Je ne sais pas qui et comment doit gérer les événements générés par la classe que j'ai écrite, mais je ne veux pas vraiment que ces gestionnaires ralentissent le travail de ma classe. C'est pourquoi, j'utiliserai la méthode BeginInvoke au lieu de Invoke. BeginInvoke génère un nouveau thread.

Remarque :Les méthodes Invoke et BeginInvoke ne sont pas membres des classes Delegate ou MulticastDelegate. Ce sont les membres de la classe générée (ou de la classe universelle décrite ci-dessus).

Maintenant, si nous changeons la méthode dans laquelle l'événement est généré, nous obtiendrons ce qui suit :

Génération d'événements multi-thread :

 void OnCounterChanged(int oldValue, int newValue) { if (CounterChanged !=null) { var délégués =CounterChanged.GetInvocationList(); for (var i =0; i )delegates[i]).BeginInvoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue), null, null); Console.WriteLine(string.Format("Event Raiser :old ={0}, new ={1}", oldValue, newValue)); } }

Les deux derniers paramètres sont nuls. Le premier est un rappel, le second est un certain paramètre. Je n'utilise pas de rappel dans cet exemple, car l'exemple est intermédiaire. Cela peut être utile pour les commentaires. Par exemple, cela peut aider la classe qui génère l'événement à déterminer si un événement a été géré et/ou s'il est nécessaire d'obtenir les résultats de cette gestion. Il peut également libérer des ressources liées au fonctionnement asynchrone.

Si nous exécutons le programme, nous obtiendrons le résultat suivant.

Je suppose qu'il est assez clair que maintenant les gestionnaires d'événements sont exécutés dans des threads séparés, c'est-à-dire que le générateur d'événements ne se soucie pas de savoir qui, comment et combien de temps gérera ses événements.

Et ici la question se pose :qu'en est-il du traitement séquentiel ? Nous avons Counter, après tout. Et s'il s'agissait d'un changement d'état en série ? Mais je ne répondrai pas à cette question, ce n'est pas le sujet de cet article. Je peux seulement dire qu'il y a plusieurs façons.

Et encore une chose. Afin de ne pas répéter les mêmes actions encore et encore, je suggère de leur créer une classe distincte.

Une classe pour la génération d'événements asynchrones

 static class AsyncEventsHelper { public static void RaiseEventAsync(EventHandler h, object sender, T e) where T :EventArgs { if (h !=null) { var délégués =h.GetInvocationList(); for (var i =0; i )delegates[i]).BeginInvoke(sender, e, h.EndInvoke, null); } } } 

Dans ce cas, nous utilisons le rappel. Il est exécuté dans le même thread que le gestionnaire. Autrement dit, une fois la méthode du gestionnaire terminée, le délégué appelle h.EndInvoke next.

Voici comment il doit être utilisé

 void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); } 

Je suppose qu'il est maintenant clair pourquoi la méthode universelle était nécessaire. Si nous décrivons des événements avec la méthode 2, cette astuce ne fonctionnera pas. Sinon, vous devrez créer vous-même l'universalité pour vos délégués.

Remarque :Pour les projets réels, je recommande de changer l'architecture des événements dans le contexte des threads. Les exemples décrits peuvent endommager le travail de l'application avec des threads et sont fournis à titre informatif uniquement.

Conclusion

Hope, j'ai réussi à décrire comment les événements fonctionnent et où travaillent les gestionnaires. Dans le prochain article, je prévois d'approfondir les résultats de la gestion des événements lorsqu'un appel asynchrone est effectué.

J'attends avec impatience vos commentaires et suggestions.