web-dev-qa-db-fra.com

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

Je crée 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'avertir les utilisateurs que le jeu est en train de s'arrêter et qu'ils doivent sauvegarder leurs données. Les sauvegardes sont attendues, pas les événements. Ainsi, lorsque le gestionnaire est appelé, la partie s’arrête avant que les gestionnaires attendus puissent terminer.

public event EventHandler<EventArgs> 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 que le rendre asynchrone est donc fondamentalement un feu et un 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 les laisser réagir.

Existe-t-il un bon moyen de remplacer eventing par quelque chose d’asynchrone que je puisse utiliser? Je ne sais pas si je devrais utiliser BeginShutdownGame et EndShutdownGame avec des rappels, mais c'est une tâche pénible, car seule la source appelante peut transmettre un rappel, et pas les éléments tiers qui se connectent. au moteur, qui est ce que je reçois avec des événements. Si le serveur appelle game.ShutdownGame(), les plug-ins du moteur et les autres composants du moteur ne peuvent transmettre leurs rappels, à moins que je ne connecte une sorte de méthode d'enregistrement en conservant une collection de rappels.

Tout conseil sur ce que la route préférée/recommandée pour descendre avec ceci serait grandement apprécié! J'ai regardé autour de moi et, dans l'ensemble, ce que j'ai vu utilise l'approche de début/fin, ce qui, à mon avis, ne satisfera pas ce que je veux faire.

Éditer

Une autre option que je considère consiste à utiliser une méthode d’enregistrement, qui prend un rappel en attente. Je parcours tous les rappels, saisis leur tâche et l'attends avec un WhenAll.

private List<Func<Task>> ShutdownCallbacks = new List<Func<Task>>();

public void RegisterShutdownCallback(Func<Task> callback)
{
    this.ShutdownCallbacks.Add(callback);
}

public async Task Shutdown()
{
    var callbackTasks = new List<Task>();
    foreach(var callback in this.ShutdownCallbacks)
    {
        callbackTasks.Add(callback());
    }

    await Task.WhenAll(callbackTasks);
}
61

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

Cela dit, si, pour une raison quelconque, vous devez ou du moins êtes fermement obligé de vous en tenir à cette conception, vous pouvez le faire de manière conviviale await.

Votre idée d’enregistrer les gestionnaires et await est bonne. Cependant, je suggérerais de rester avec le paradigme d’événement existant, car cela maintiendrait l’expressivité des événements dans votre code. Le principal est que vous deviez vous écarter du type de délégué standard basé sur EventHandler et utiliser un type de délégué renvoyant un Task afin que vous puissiez await gérer les gestionnaires.

Voici un exemple simple illustrant ce que je veux dire:

class A
{
    public event Func<object, EventArgs, Task> Shutdown;

    public async Task OnShutdown()
    {
        Func<object, EventArgs, Task> 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<object, EventArgs, Task>)invocationList[i])(this, EventArgs.Empty);
        }

        await Task.WhenAll(handlerTasks);
    }
}

La méthode OnShutdown(), après avoir effectué la procédure standard "obtenir une copie locale de l'instance de délégué d'événement", appelle d'abord tous les gestionnaires, puis attend tous les Tasks renvoyés (après les avoir enregistrés un tableau local comme les gestionnaires sont appelés).

Voici un court programme de la console illustrant l'utilisation:

class Program
{
    static void Main(string[] 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 si C # n'aurait pas pu trouver un moyen de résumer cela un peu. Cela aurait peut-être été un changement trop compliqué, mais le mélange actuel des anciens gestionnaires de style void-return et de la nouvelle fonctionnalité async/await semble un peu gênant. Ce qui précède fonctionne (et fonctionne bien, à mon humble avis), mais il aurait été agréable d’avoir un meilleur support de CLR et/ou de langue pour le scénario (c’est-à-dire pouvoir attendre un délégué de multidiffusion et demander au compilateur C # de le transformer en un appel à WhenAll()).

74
Peter Duniho
internal static class EventExtensions
{
    public static void InvokeAsync<TEventArgs>(this EventHandler<TEventArgs> @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<TEventArgs>) t;
            handler.BeginInvoke(sender, args, ar, userObject);
        }
    }
}

exemple:

    public event EventHandler<CodeGenEventArgs> CodeGenClick;

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

Remarque: Ceci est asynchrone afin que le gestionnaire d'événements puisse compromettre le thread d'interface utilisateur. Le gestionnaire d'événements (abonné) ne devrait pas travailler avec l'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. Invoquer l'événement de votre fournisseur:

    DoSomething.InvokeAsync (new MyEventArgs (), this, ar => {callback appelé lorsque l'opération est terminée (synchronisez l'interface utilisateur si nécessaire ici),}, null);

  3. inscrire l'événement par client comme vous le feriez normalement

2
Martin.Martinsson

Il est vrai que les événements sont par nature inattendus, vous devez donc les contourner.

Une solution que j’avais utilisée dans le passé utilise n sémaphore pour attendre que toutes les entrées qu’il contient soient libérées. Dans ma situation, je n’avais qu’un seul événement abonné pour pouvoir le coder en dur en tant que new SemaphoreSlim(0, 1), mais dans votre cas, vous voudrez peut-être remplacer le getter/setter de votre événement et conserver un compteur du nombre d’abonnés. peut définir dynamiquement le nombre maximal de threads simultanés.

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

Cela bloquerait essentiellement votre programme jusqu'à ce que tous les abonnés aux événements aient terminé.

Vous pouvez également envisager de proposer un événement à la GameShutDownFinished à 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émaphore et simplement utiliser Semaphore.Wait() pour bloquer le thread. Au lieu d'avoir à vérifier si toutes les entrées ont été effacées, vous devez maintenant attendre qu'une place soit libérée (mais il ne devrait y avoir qu'un moment où toutes les places sont libérées à la fois).

1
Jeroen Vannevel

Peter L'exemple est excellent, je viens de le simplifier un peu en utilisant LINQ et ses extensions:

public static class AsynchronousEventExtensions
{
    public static Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
        where TEventArgs : EventArgs
    {
        if (handlers != null)
        {
            return Task.WhenAll(handlers.GetInvocationList()
                .OfType<Func<TSource, TEventArgs, Task>>()
                .Select(h => h(source, args)));
        }

        return Task.CompletedTask;
    }
}

Il peut être judicieux d’ajouter un délai d’attente. Pour déclencher l'événement, appelez Raise extension:

public event Func<A, EventArgs, Task> Shutdown;

private async Task SomeMethod()
{
    ...

    await Shutdown.Raise(this, EventArgs.Empty);

    ...
}

Mais vous devez savoir que, contrairement aux événements synchrones, cette implémentation appelle les gestionnaires simultanément. Cela peut poser problème si les gestionnaires doivent être exécutés de manière strictement consécutive comme ils le font souvent, par exemple. un prochain gestionnaire dépend des résultats du précédent:

someInstance.Shutdown += OnShutdown1;
someInstance.Shutdown += OnShutdown2;

...

private async Task OnShutdown1(SomeClass source, MyEventArgs args)
{
    if (!args.IsProcessed)
    {
        // An operation
        await Task.Delay(123);
        args.IsProcessed = true;
    }
}

private async Task OnShutdown2(SomeClass source, MyEventArgs args)
{
    // OnShutdown2 will start execution the moment OnShutdown1 hits await
    // and will proceed to the operation, which is not the desired behavior.
    // Or it can be just a concurrent DB query using the same connection
    // which can result in an exception thrown base on the provider
    // and connection string options
    if (!args.IsProcessed)
    {
        // An operation
        await Task.Delay(123);
        args.IsProcessed = true;
    }
}

Vous feriez mieux de changer la méthode d'extension pour appeler les gestionnaires consécutivement:

public static class AsynchronousEventExtensions
{
    public static async Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
        where TEventArgs : EventArgs
    {
        if (handlers != null)
        {
            foreach (Func<TSource, TEventArgs, Task> handler in handlers.GetInvocationList())
            {
                await handler(source, args);
            }
        }
    }
}
1
Kosta_Arnorsky

Je sais que l’opérateur demandait spécifiquement l’utilisation de tâches asynchrones et asynchrones, mais voici une alternative qui évite aux gestionnaires de retourner une valeur. Le code est basé sur l'exemple de Peter Duniho. D'abord l'équivalent classe A (écrasé 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<Delegate>(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(string[] 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.

1
jetbadger