Comment puis-je attendre des événements en C #?

Je suis en train de créer une classe qui comporte une série d’événements, dont GameShuttingDown . Lorsque cet événement est déclenché, je dois appeler le gestionnaire d’événements. Le but de cet événement est d’informer les utilisateurs que le jeu est en cours d’arrêt et qu’ils doivent enregistrer leurs données. Les sauvegardes sont attendues et les événements ne le sont pas. Ainsi, lorsque le gestionnaire est appelé, le jeu s’arrête avant que les gestionnaires attendus puissent se terminer.

 public event EventHandler GameShuttingDown; public virtual async Task ShutdownGame() { await this.NotifyGameShuttingDown(); await this.SaveWorlds(); this.NotifyGameShutDown(); } private async Task SaveWorlds() { foreach (DefaultWorld world in this.Worlds) { await this.worldService.SaveWorld(world); } } protected virtual void NotifyGameShuttingDown() { var handler = this.GameShuttingDown; if (handler == null) { return; } handler(this, new EventArgs()); } 

Inscription à l’événement

 // The game gets shut down before this completes because of the nature of how events work DefaultGame.GameShuttingDown += async (sender, args) => await this.repo.Save(blah); 

Je comprends que la signature pour les événements est void EventName et donc le rendre asynchrone est fondamentalement le feu et l’oubli. Mon moteur utilise beaucoup les événements pour informer les développeurs tiers (et plusieurs composants internes) que des événements ont lieu dans le moteur et leur permettent de réagir.

Existe-t-il un bon moyen de remplacer les événements par des éléments asynchrones que je peux utiliser? Je ne sais pas si je devrais utiliser BeginShutdownGame et EndShutdownGame avec des callbacks, mais c’est EndShutdownGame parce que seule la source appelante peut transmettre un rappel, et non des éléments tiers qui se connectent au moteur. avec des événements. Si le serveur appelle game.ShutdownGame() , les plugins du moteur et / ou les autres composants du moteur ne peuvent pas transmettre leurs rappels, à moins que je ne mette en place une méthode d’enregistrement, conservant une collection de rappels.

Tout conseil sur la voie préférée / recommandée pour descendre avec cela serait grandement apprécié! J’ai regardé autour de moi et, pour l’essentiel, j’ai utilisé l’approche Begin / End qui, selon moi, ne satisferait pas ce que je veux faire.

modifier

Une autre option que je considère utilise une méthode d’enregistrement, qui prend un rappel attendu. Je répète tous les rappels, récupère leur tâche et attend avec un WhenAll .

 private List<Func> ShutdownCallbacks = new List<Func>(); public void RegisterShutdownCallback(Func callback) { this.ShutdownCallbacks.Add(callback); } public async Task Shutdown() { var callbackTasks = new List(); foreach(var callback in this.ShutdownCallbacks) { callbackTasks.Add(callback()); } await Task.WhenAll(callbackTasks); } 

Personnellement, je pense qu’avoir des gestionnaires d’événements async n’est peut-être pas le meilleur choix de conception, la raison étant le problème que vous rencontrez. Avec les gestionnaires synchrones, il est sortingvial de savoir quand ils se terminent.

Cela dit, si, pour une raison quelconque, vous devez ou êtes fortement obligé de respecter cette conception, vous pouvez le faire de manière conviviale.

Votre idée d’enregistrer les gestionnaires et de les await est une bonne idée. Cependant, je suggérerais de restr avec le paradigme d’événement existant, car cela gardera l’expressivité des événements dans votre code. L’essentiel est que vous deviez dévier du type délégué EventHandler standard et utiliser un type de délégué qui renvoie une Task afin que vous puissiez await les gestionnaires.

Voici un exemple simple illustrant ce que je veux dire:

 class A { public event Func Shutdown; public async Task OnShutdown() { Func handler = Shutdown; if (handler == null) { return; } Delegate[] invocationList = handler.GetInvocationList(); Task[] handlerTasks = new Task[invocationList.Length]; for (int i = 0; i < invocationList.Length; i++) { handlerTasks[i] = ((Func)invocationList[i])(this, EventArgs.Empty); } await Task.WhenAll(handlerTasks); } } 

La méthode OnShutdown() , après avoir OnShutdown() la norme “Obtenir une copie locale de l’instance du délégué d’événement”, appelle d’abord tous les gestionnaires, puis attend toutes les Tasks renvoyées (les ayant enregistrées dans un tableau local lorsque les gestionnaires sont appelés). .

Voici un petit programme de console illustrant l’utilisation:

 class Program { static void Main(ssortingng[] args) { A a = new A(); a.Shutdown += Handler1; a.Shutdown += Handler2; a.Shutdown += Handler3; a.OnShutdown().Wait(); } static async Task Handler1(object sender, EventArgs e) { Console.WriteLine("Starting shutdown handler #1"); await Task.Delay(1000); Console.WriteLine("Done with shutdown handler #1"); } static async Task Handler2(object sender, EventArgs e) { Console.WriteLine("Starting shutdown handler #2"); await Task.Delay(5000); Console.WriteLine("Done with shutdown handler #2"); } static async Task Handler3(object sender, EventArgs e) { Console.WriteLine("Starting shutdown handler #3"); await Task.Delay(2000); Console.WriteLine("Done with shutdown handler #3"); } } 

Après avoir parcouru cet exemple, je me demande maintenant s’il ne pouvait y avoir un moyen pour C # de faire un résumé de ceci. Cela aurait peut-être été un changement trop compliqué, mais le mélange actuel des anciens gestionnaires d’événements de retour à zéro et de la nouvelle fonctionnalité async / attendue semble un peu gênant. Le fonctionnement ci-dessus fonctionne (et fonctionne bien à WhenAll() ), mais il aurait été intéressant d’avoir un meilleur support CLR et / ou langage pour le scénario (pouvoir attendre un délégué multicast et demander au compilateur C # d’en faire un appel à WhenAll() ).

 internal static class EventExtensions { public static void InvokeAsync(this EventHandler @event, object sender, TEventArgs args, AsyncCallback ar, object userObject = null) where TEventArgs : class { var listeners = @event.GetInvocationList(); foreach (var t in listeners) { var handler = (EventHandler) t; handler.BeginInvoke(sender, args, ar, userObject); } } } 

Exemple:

  public event EventHandler CodeGenClick; private void CodeGenClickAsync(CodeGenEventArgs args) { CodeGenClick.InvokeAsync(this, args, ar => { InvokeUI(() => { if (args.Code.IsNotNullOrEmpty()) { var oldValue = (ssortingng) gv.GetRowCellValue(gv.FocusedRowHandle, nameof(License.Code)); if (oldValue != args.Code) gv.SetRowCellValue(gv.FocusedRowHandle, nameof(License.Code), args.Code); } }); }); } 

Remarque: Ceci est async, donc le gestionnaire d’événements peut compromettre le thread d’interface utilisateur. Le gestionnaire d’événement (abonné) ne doit faire aucun travail d’interface utilisateur. Cela n’aurait pas beaucoup de sens autrement.

  1. déclarez votre événement dans votre fournisseur d’événements:

    événement public EventHandler DoSomething;

  2. Appelez l’événement de votre fournisseur:

    DoSomething.InvokeAsync (new MyEventArgs (), this, ar => {callback appelé une fois terminé (synchroniser l’interface utilisateur si nécessaire ici!)}, Null);

  3. abonnez-vous à l’événement par le client comme vous le feriez normalement

C’est vrai, les événements sont insortingnsèquement inattendus, vous devrez donc les contourner.

Une solution que j’ai utilisée dans le passé consiste à utiliser un sémaphore pour attendre la publication de toutes les entrées. Dans ma situation, je n’avais qu’un seul événement souscrit, donc je pouvais le coder en tant que new SemaphoreSlim(0, 1) mais dans votre cas, vous pourriez vouloir remplacer le getter / setter pour votre événement et garder un compteur d’abonnés. peut définir dynamicment la quantité maximale de threads simultanés.

Ensuite, vous passez une entrée de sémaphore à chacun des abonnés et vous les laissez faire jusqu’à ce que SemaphoreSlim.CurrentCount == amountOfSubscribers (aka: tous les spots ont été libérés).

Cela bloquerait essentiellement votre programme jusqu’à ce que tous les abonnés à l’événement soient terminés.

Vous pouvez également envisager de fournir un événement à la GameShutDownFinished pour vos abonnés, qu’ils doivent appeler lorsqu’ils ont terminé leur tâche de fin de jeu. Combiné à la surcharge SemaphoreSlim.Release(int) vous pouvez maintenant effacer toutes les entrées de sémaphores et simplement utiliser Semaphore.Wait() pour bloquer le thread. Au lieu d’avoir à vérifier si toutes les entrées ont été effacées ou non, vous devez attendre jusqu’à ce qu’un endroit soit libéré (mais il ne devrait y avoir qu’un seul moment où tous les spots sont libérés en même temps).

Je sais que l’op demandait spécifiquement à utiliser l’async et les tâches pour cela, mais voici une alternative qui signifie que les gestionnaires n’ont pas besoin de retourner une valeur. Le code est basé sur l’exemple de Peter Duniho. D’abord la classe équivalente A (écrasée un peu pour s’adapter): –

 class A { public delegate void ShutdownEventHandler(EventArgs e); public event ShutdownEventHandler ShutdownEvent; public void OnShutdownEvent(EventArgs e) { ShutdownEventHandler handler = ShutdownEvent; if (handler == null) { return; } Delegate[] invocationList = handler.GetInvocationList(); Parallel.ForEach(invocationList, (hndler) => { ((ShutdownEventHandler)hndler)(e); }); } } 

Une application console simple pour montrer son utilisation …

 using System; using System.Threading; using System.Threading.Tasks; ... class Program { static void Main(ssortingng[] args) { A a = new A(); a.ShutdownEvent += Handler1; a.ShutdownEvent += Handler2; a.ShutdownEvent += Handler3; a.OnShutdownEvent(new EventArgs()); Console.WriteLine("Handlers should all be done now."); Console.ReadKey(); } static void handlerCore( int id, int offset, int num ) { Console.WriteLine("Starting shutdown handler #{0}", id); int step = 200; Thread.Sleep(offset); for( int i = 0; i < num; i += step) { Thread.Sleep(step); Console.WriteLine("...Handler #{0} working - {1}/{2}", id, i, num); } Console.WriteLine("Done with shutdown handler #{0}", id); } static void Handler1(EventArgs e) { handlerCore(1, 7, 5000); } static void Handler2(EventArgs e) { handlerCore(2, 5, 3000); } static void Handler3(EventArgs e) { handlerCore(3, 3, 1000); } } 

J'espère que cela est utile à quelqu'un.