it-swarm.com.de

Wie warte ich auf Ereignisse in C #?

Ich erstelle eine Klasse mit einer Reihe von Ereignissen, von denen eines GameShuttingDown ist. Wenn dieses Ereignis ausgelöst wird, muss ich den Ereignishandler aufrufen. Der Zweck dieses Ereignisses ist es, Benutzer zu benachrichtigen, dass das Spiel heruntergefahren wird und sie ihre Daten speichern müssen. Die Speichervorgänge sind absehbar und die Ereignisse nicht. Wenn der Handler gerufen wird, wird das Spiel beendet, bevor die erwarteten Handler fertig sind.

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());
}

Veranstaltungsanmeldung

// 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);

Ich verstehe, dass die Signatur für Ereignisse void EventName Lautet, und es ist im Grunde genommen Feuer und Vergessen, sie asynchron zu machen. Meine Engine nutzt Eventing in hohem Maße, um Entwickler von Drittanbietern (und mehrere interne Komponenten) darüber zu informieren, dass Ereignisse innerhalb der Engine stattfinden, und sie auf diese reagieren zu lassen.

Gibt es eine gute Möglichkeit, das Eventing durch etwas Asynchrones zu ersetzen, das ich verwenden kann? Ich bin nicht sicher, ob ich BeginShutdownGame und EndShutdownGame mit Rückrufen verwenden soll, aber das ist ein Schmerz, weil dann nur die aufrufende Quelle einen Rückruf weiterleiten kann, und keine Drittanbieter-Inhalte, die eingesteckt werden zum Motor, was ich mit Ereignissen bekomme. Wenn der Server game.ShutdownGame() aufruft, können Modul-Plug-ins und andere Komponenten im Modul ihre Rückrufe nur weiterleiten, wenn ich eine Registrierungsmethode verkable und eine Sammlung von Rückrufen behalte.

Wir würden uns sehr freuen, wenn Sie uns einen Rat geben würden, welche Route Sie bevorzugen/empfehlen! Ich habe mich umgesehen und größtenteils gesehen, dass ich den Ansatz Begin/End verwende, von dem ich nicht glaube, dass er das befriedigt, was ich tun möchte.

Bearbeiten

Eine andere Option, die ich in Betracht ziehe, ist die Verwendung einer Registrierungsmethode, für die ein abwartender Rückruf erforderlich ist. Ich durchlaufe alle Rückrufe, nehme ihre Aufgabe und warte mit einem 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

Persönlich denke ich, dass die Verwendung von async - Ereignishandlern möglicherweise nicht die beste Wahl für das Design ist, nicht zuletzt, weil dies das eigentliche Problem ist, das Sie haben. Bei synchronen Handlern ist es trivial zu wissen, wann sie abgeschlossen sind.

Das heißt, wenn Sie aus irgendeinem Grund gezwungen sein müssen, sich an dieses Design zu halten, können Sie dies auf eine await - freundliche Weise tun.

Ihre Idee, Handler zu registrieren und await, ist gut. Ich würde jedoch vorschlagen, das vorhandene Ereignisparadigma beizubehalten, da dadurch die Aussagekraft von Ereignissen in Ihrem Code erhalten bleibt. Die Hauptsache ist, dass Sie vom standardmäßigen EventHandler - basierten Delegattyp abweichen und einen Delegattyp verwenden müssen, der einen Task zurückgibt, damit Sie die Handler await können.

Hier ist ein einfaches Beispiel, das zeigt, was ich meine:

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);
    }
}

Die Methode OnShutdown() ruft nach Ausführung des Standards "Lokale Kopie der Ereignisdelegateninstanz abrufen" zuerst alle Handler auf und wartet dann auf alle zurückgegebenen Tasks (nachdem sie in a gespeichert wurden) lokales Array, wenn die Handler aufgerufen werden).

Hier ist ein kurzes Konsolenprogramm, das die Verwendung veranschaulicht:

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");
    }
}

Nachdem ich dieses Beispiel durchgesehen habe, frage ich mich nun, ob es für C # keine Möglichkeit gegeben hat, dies ein wenig zu abstrahieren. Vielleicht wäre es eine zu komplizierte Änderung gewesen, aber der aktuelle Mix aus void - wiederkehrenden Event-Handlern und der neuen async/await -Funktion scheint ein bisschen zu sein peinlich. Das oben Genannte funktioniert (und funktioniert auch, IMHO), aber es wäre schön gewesen, eine bessere CLR- und/oder Sprachunterstützung für das Szenario zu haben (dh in der Lage zu sein, einen Multicast-Delegaten zu erwarten und den C # -Compiler zu veranlassen, dies in einen Aufruf an 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);
        }
    }
}

beispiel:

    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);
                }
            });
        });
    }

Hinweis: Dies ist asynchron, sodass der Ereignishandler möglicherweise den UI-Thread gefährdet. Der Event-Handler (Abonnent) sollte keine UI-Arbeit leisten. Sonst würde es nicht viel Sinn machen.

  1. erklären Sie Ihre Veranstaltung bei Ihrem Veranstalter:

    öffentliches Ereignis EventHandler DoSomething;

  2. Rufen Sie bei Ihrem Provider Folgendes auf:

    DoSomething.InvokeAsync (new MyEventArgs (), this, ar => {Rückruf wird nach Abschluss aufgerufen (Benutzeroberfläche bei Bedarf hier synchronisieren!)}, Null);

  3. abonnieren Sie die Veranstaltung nach Kunden wie gewohnt

2

Es ist wahr, Ereignisse sind von Natur aus nicht abwartbar, also müssen Sie sie umgehen.

Eine Lösung, die ich in der Vergangenheit verwendet habe, ist ein Semaphor , um darauf zu warten, dass alle darin enthaltenen Einträge freigegeben werden. In meiner Situation hatte ich nur ein abonniertes Ereignis, so dass ich es als new SemaphoreSlim(0, 1) fest codieren konnte, aber in Ihrem Fall möchten Sie möglicherweise den Getter/Setter für Ihr Ereignis außer Kraft setzen und einen Zähler dafür führen, wie viele Abonnenten Sie haben kann die maximale Anzahl gleichzeitiger Threads dynamisch festlegen.

Anschließend übergeben Sie jedem Abonnenten einen Semaphoreintrag und lassen ihn sein Ding machen, bis SemaphoreSlim.CurrentCount == amountOfSubscribers (Auch bekannt als: alle Stellen wurden freigegeben).

Dies würde Ihr Programm im Wesentlichen blockieren, bis alle Event-Abonnenten fertig sind.

Möglicherweise möchten Sie Ihren Abonnenten auch ein Ereignis à la GameShutDownFinished zur Verfügung stellen, das sie anrufen müssen, wenn sie mit ihrer Aufgabe am Ende des Spiels fertig sind. In Kombination mit der Überladung SemaphoreSlim.Release(int) können Sie jetzt alle Semaphoreingaben löschen und einfach Semaphore.Wait() verwenden, um den Thread zu blockieren. Anstatt zu prüfen, ob alle Einträge gelöscht wurden oder nicht, warten Sie jetzt, bis ein Platz frei ist (es sollte jedoch nur einen Moment geben, in dem alle Plätze auf einmal frei sind).

1
Jeroen Vannevel

Peter's Beispiel ist großartig, ich habe es mit LINQ und Erweiterungen ein wenig vereinfacht:

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;
    }
}

Es kann eine gute Idee sein, ein Timeout hinzuzufügen. Um das Ereignis auszulösen, rufen Sie die Raise-Erweiterung auf:

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

private async Task SomeMethod()
{
    ...

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

    ...
}

Beachten Sie jedoch, dass diese Implementierung im Gegensatz zu synchronen Ereignissen Handler gleichzeitig aufruft. Es kann ein Problem sein, wenn Handler streng nacheinander ausgeführt werden müssen, was sie oft tun, z. Ein nächster Handler hängt von den Ergebnissen des vorherigen ab:

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;
    }
}

Sie sollten die Erweiterungsmethode so ändern, dass Handler nacheinander aufgerufen werden:

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

Ich weiß, dass die Operation speziell nach der Verwendung von Async und Tasks gefragt hat, aber hier ist eine Alternative, bei der die Handler keinen Wert zurückgeben müssen. Der Code basiert auf dem Beispiel von Peter Duniho. Zuerst die äquivalente Klasse A (etwas zusammengedrückt, um zu passen):

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); });
    }
}

Eine einfache Konsolenanwendung, um ihre Verwendung zu demonstrieren ...

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); }
}

Ich hoffe, dass dies jemandem nützlich ist.

1
jetbadger