Les événements C # sont-ils synchrones?

104

Il y a deux parties à cette question:

  1. Ne soulevant un événement bloque le fil, ou faut - il commencer l' exécution des gestionnaires d' événements de manière asynchrone et le fil se poursuit en même temps?

  2. Les EventHandlers individuels (abonnés à l'événement) sont-ils exécutés de manière synchrone les uns après les autres ou sont-ils exécutés de manière asynchrone sans aucune garantie que les autres ne s'exécutent pas en même temps?

Alexander Bird
la source

Réponses:

37

Pour répondre à tes questions:

  1. Le déclenchement d'un événement bloque le thread si les gestionnaires d'événements sont tous implémentés de manière synchrone.
  2. Les gestionnaires d'événements sont exécutés séquentiellement, les uns après les autres, dans l'ordre dans lequel ils sont abonnés à l'événement.

J'étais moi aussi curieux de connaître le mécanisme interne de eventet ses opérations connexes. J'ai donc écrit un programme simple et utilisé ildasmpour fouiller autour de son implémentation.

La réponse courte est

  • il n'y a aucune opération asynchrone impliquée dans l'abonnement ou l'appel des événements.
  • l'événement est implémenté avec un champ de délégué de support du même type de délégué
  • l'abonnement se fait avec Delegate.Combine()
  • la désinscription se fait avec Delegate.Remove()
  • L'appel se fait simplement en invoquant le délégué combiné final

Voici ce que j'ai fait. Le programme que j'ai utilisé:

public class Foo
{
    // cool, it can return a value! which value it returns if there're multiple 
    // subscribers? answer (by trying): the last subscriber.
    public event Func<int, string> OnCall;
    private int val = 1;

    public void Do()
    {
        if (OnCall != null) 
        {
            var res = OnCall(val++);
            Console.WriteLine($"publisher got back a {res}");
        }
    }
}

public class Program
{
    static void Main(string[] args)
    {
        var foo = new Foo();

        foo.OnCall += i =>
        {
            Console.WriteLine($"sub2: I've got a {i}");
            return "sub2";
        };

        foo.OnCall += i =>
        {
            Console.WriteLine($"sub1: I've got a {i}");
            return "sub1";
        };

        foo.Do();
        foo.Do();
    }
}

Voici l'implémentation de Foo:

entrez la description de l'image ici

Notez qu'il existe un champ OnCall et un événement OnCall . Le champ OnCallest évidemment la propriété de support. Et c'est simplement un Func<int, string>, rien d'extraordinaire ici.

Maintenant, les parties intéressantes sont:

  • add_OnCall(Func<int, string>)
  • remove_OnCall(Func<int, string>)
  • et comment OnCallest invoqué dansDo()

Comment l'abonnement et la désinscription sont-ils mis en œuvre?

Voici l' add_OnCallimplémentation abrégée dans CIL. La partie intéressante est qu'il utilise Delegate.Combinepour concaténer deux délégués.

.method public hidebysig specialname instance void 
        add_OnCall(class [mscorlib]System.Func`2<int32,string> 'value') cil managed
{
  // ...
  .locals init (class [mscorlib]System.Func`2<int32,string> V_0,
           class [mscorlib]System.Func`2<int32,string> V_1,
           class [mscorlib]System.Func`2<int32,string> V_2)
  IL_0000:  ldarg.0
  IL_0001:  ldfld      class [mscorlib]System.Func`2<int32,string> ConsoleApp1.Foo::OnCall
  // ...
  IL_000b:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  // ...
} // end of method Foo::add_OnCall

De même, Delegate.Removeest utilisé dans remove_OnCall.

Comment un événement est-il invoqué?

Pour invoquer OnCallin Do(), il appelle simplement le délégué concaténé final après le chargement de l'arg:

IL_0026:  callvirt   instance !1 class [mscorlib]System.Func`2<int32,string>::Invoke(!0)

Comment exactement un abonné s'abonne-t-il à un événement?

Et enfin, Mainsans surprise, l'inscription à l' OnCallévénement se fait en appelant add_OnCallmethod sur l' Fooinstance.

KFL
la source
3
Bien joué!! Cela fait si longtemps que je n'ai pas posé cette question. Si vous pouvez mettre le verbiage en haut qui répond directement à ma question en deux parties (c'est-à-dire, "la réponse n ° 1 est non; la réponse n ° 2 est non") alors je vais en faire la réponse officielle. Je parie que votre message contient toutes les pièces nécessaires pour répondre à mes questions originales, mais comme je n'utilise plus C # (et d'autres googleurs peuvent être nouveaux dans ces concepts), c'est pourquoi je demande un verbiage qui rend les réponses évidentes.
Alexander Bird
Merci @AlexanderBird, je viens de le modifier pour mettre les réponses en haut.
KFL
@KFL, ce n'est toujours pas clair, j'étais sur le point de laisser le même commentaire qu'Alex. Un simple "Oui, ils sont synchrones", serait utile
johnny 5
71

Ceci est une réponse générale et reflète le comportement par défaut:

  1. Oui, il bloque le thread, si les méthodes qui s'abonnent à l'événement ne sont pas asynchrones.
  2. Ils sont exécutés les uns après les autres. Cela a une autre particularité: si un gestionnaire d'événements lève une exception, les gestionnaires d'événements non encore exécutés ne seront pas exécutés.

Cela dit, chaque classe qui fournit des événements peut choisir d'implémenter son événement de manière asynchrone. IDesign fournit une classe appelée EventsHelperqui simplifie cela.

[Note] ce lien vous oblige à fournir une adresse e-mail pour télécharger la classe EventsHelper. (Je ne suis en aucun cas affilié)

Daniel Hilgarth
la source
J'ai lu quelques messages sur le forum, dont deux ont contredit le premier point, sans fournir de raison valable. Je ne doute pas de votre réponse, (cela correspond à ce que j'ai vécu jusqu'à présent) y a-t-il une documentation officielle sur le premier point? J'ai besoin d'en être certain, mais j'ai du mal à trouver quoi que ce soit d'officiel sur cette question précise.
Adam LS
@ AdamL.S. Il s'agit de savoir comment l'événement est appelé. Cela dépend donc vraiment de la classe qui organise l'événement.
Daniel Hilgarth
14

Les délégués abonnés à l'événement sont appelés de manière synchrone dans l'ordre dans lequel ils ont été ajoutés. Si l'un des délégués lève une exception, les suivants ne seront pas appelés.

Étant donné que les événements sont définis avec des délégués multicast, vous pouvez écrire votre propre mécanisme de déclenchement en utilisant

Delegate.GetInvocationList();

et appeler les délégués de manière asynchrone;

Vladimir
la source
12

Les événements ne sont que des tableaux de délégués. Tant que l'appel du délégué est synchrone, les événements sont également synchrones.

Andrey Agibalov
la source
3

Les événements en C # s'exécutent de manière synchrone (dans les deux cas), tant que vous ne démarrez pas un deuxième thread manuellement.

Doc Brown
la source
Qu'en est-il que j'utilise un gestionnaire d'événements asynchrones? Sera-t-il ensuite exécuté dans un autre thread? J'ai entendu parler de "Async tout le chemin", mais il semble que les gestionnaires d'événements async ont leur propre thread? Je ne comprends pas: / Pouvez-vous s'il vous plaît m'éclairer?
Ailier Sendon
3

Les événements sont synchrones. C'est pourquoi le cycle de vie des événements fonctionne comme il le fait. Les inits se produisent avant les chargements, les chargements se produisent avant les rendus, etc.

Si aucun gestionnaire n'est spécifié pour un événement, le cycle éclate simplement. Si plus d'un gestionnaire est spécifié, ils seront appelés dans l'ordre et l'un ne peut pas continuer tant que l'autre n'est pas complètement terminé.

Même les appels asynchrones sont synchrones dans une certaine mesure. Il serait impossible d'appeler la fin avant que le début ne soit terminé.

Joël Etherton
la source