Ich erstelle eine Klasse, die eine Reihe von Ereignissen hat, eines davon ist GameShuttingDown
. Wenn dieses Ereignis ausgelöst wird, muss ich den Ereignishandler aufrufen. Der Zweck dieses Ereignisses besteht darin, Benutzer zu benachrichtigen, dass das Spiel heruntergefahren wird und sie ihre Daten speichern müssen. Die Speicherungen sind erwartbar und Ereignisse nicht. Wenn der Handler gerufen wird, wird das Spiel beendet, bevor die erwarteten Handler abgeschlossen werden können.
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
so ist, dass es im Grunde genommen Feuer und Vergessen ist, 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 in der Engine stattfinden, und sie auf diese reagieren zu lassen.
Gibt es einen guten Weg, um Eventing durch etwas Asynchrones zu ersetzen, das ich verwenden kann? Ich bin mir nicht sicher, ob ich Rückrufe verwenden BeginShutdownGame
und verwenden soll EndShutdownGame
, aber das ist ein Problem, da dann nur die anrufende Quelle einen Rückruf weiterleiten kann und keine Inhalte von Drittanbietern, die an die Engine angeschlossen werden, was ich bei Ereignissen bekomme . Wenn der Server anruft game.ShutdownGame()
, können Engine-Plugins und andere Komponenten innerhalb der Engine ihre Rückrufe nicht weiterleiten , es sei denn, ich verkabele eine Registrierungsmethode und behalte eine Sammlung von Rückrufen.
Jeder Rat, was die bevorzugte / empfohlene Route ist, wäre sehr dankbar! Ich habe mich umgesehen und zum größten Teil habe ich gesehen, dass ich den Anfang / Ende-Ansatz 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, die einen erwarteten Rückruf erfordert. Ich durchlaufe alle Rückrufe, greife nach ihrer 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);
}
quelle
Task
Rückgabetyp für die Delegatensignatur. und dann auf derAntworten:
Persönlich denke ich, dass
async
Event-Handler möglicherweise nicht die beste Wahl für das Design sind, nicht zuletzt, weil der Grund genau das 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 sind oder zumindest stark gezwungen sind, an diesem Design
await
festzuhalten , können Sie dies auf eine freundliche Art und Weise tun .Ihre Idee, Handler und
await
sie zu registrieren , ist gut. Ich würde jedoch vorschlagen, am bestehenden Ereignisparadigma festzuhalten, da dadurch die Ausdruckskraft von Ereignissen in Ihrem Code erhalten bleibt. Die Hauptsache ist, dass Sie vom standardbasiertenEventHandler
Delegatentyp abweichen und einen Delegatentyp verwenden müssen, der a zurückgibt,Task
damit Sieawait
die Handler 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); } }
Das
OnShutdown()
Verfahren, nach dem Standard zu tun „erhält lokale Kopie der Veranstaltung Delegatinstanz“, zuerst ruft die Handler alle, und wartet dann auf alle zurückTasks
(eine lokale Array als die Handler aufgerufen wird gespeichert haben).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 jetzt, ob es für C # keine Möglichkeit gegeben hätte, dies ein wenig zu abstrahieren. Vielleicht wäre eine Änderung zu kompliziert gewesen, aber die aktuelle Mischung aus den
void
Ereignishandlern im alten Stil und der neuenasync
/await
Funktion scheint etwas umständlich. Das Obige funktioniert (und funktioniert meiner Meinung nach gut), 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, auf einen Multicast-Delegaten zu warten und den C # -Compiler dazu zu bringen, dies in einen Aufruf zu verwandelnWhenAll()
). .quelle
await Task.WhenAll(handler.GetInvocationList().Select(invocation => ((Func<object, EventArgs, Task>)invocation)(this, EventArgs.Empty)));
handlerTasks = Array.ConvertAll(invocationList, invocation => ((Func<object, EventArgs, Task>)invocation)(this, EventArgs.Empty)));
Peters Beispiel ist großartig, ich habe es nur ein wenig mit LINQ und Erweiterungen 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, eine Zeitüberschreitung 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); ... }
Sie müssen sich jedoch bewusst sein, 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 häufig tun, z. B. hängt ein nächster Handler 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); } } } }
quelle
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.
Erklären Sie Ihre Veranstaltung bei Ihrem Veranstalter:
öffentliche Veranstaltung EventHandler DoSomething;
Rufen Sie das Ereignis Ihres Providers auf:
DoSomething.InvokeAsync (new MyEventArgs (), this, ar => {Rückruf wird aufgerufen, wenn der Vorgang abgeschlossen ist (Benutzeroberfläche bei Bedarf hier synchronisieren!)}, Null);
Abonnieren Sie die Veranstaltung wie gewohnt vom Client
quelle
using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Example { // delegate as alternative standard EventHandler public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e, CancellationToken token); public class ExampleObject { // use as regular event field public event AsyncEventHandler<EventArgs> AsyncEvent; // invoke using the extension method public async Task InvokeEventAsync(CancellationToken token) { await this.AsyncEvent.InvokeAsync(this, EventArgs.Empty, token); } // subscribe (add a listener) with regular syntax public static async Task UsageAsync() { var item = new ExampleObject(); item.AsyncEvent += (sender, e, token) => Task.CompletedTask; await item.InvokeEventAsync(CancellationToken.None); } } public static class AsynEventHandlerExtensions { // invoke a async event (with null-checking) public static async Task InvokeAsync<TEventArgs>(this AsyncEventHandler<TEventArgs> handler, object sender, TEventArgs args, CancellationToken token) { var delegates = handler?.GetInvocationList(); if (delegates?.Length > 0) { var tasks = delegates .Cast<AsyncEventHandler<TEventArgs>>() .Select(e => e.Invoke(sender, args, token)); await Task.WhenAll(tasks); } } } }
quelle
Es ist wahr, Ereignisse sind von Natur aus nicht zu erwarten, sodass Sie sie umgehen müssen.
Eine Lösung, die ich in der Vergangenheit verwendet habe, ist die Verwendung eines Semaphors , um darauf zu warten, dass alle darin enthaltenen Einträge freigegeben werden. In meiner Situation hatte ich nur ein abonniertes Ereignis, sodass ich es fest codieren
new SemaphoreSlim(0, 1)
konnte. In Ihrem Fall möchten Sie jedoch möglicherweise den Getter / Setter für Ihr Ereignis überschreiben und einen Zähler für die Anzahl der Abonnenten behalten, damit Sie die maximale Anzahl dynamisch festlegen können gleichzeitige Threads.Anschließend übergeben Sie jedem Abonnenten einen Semaphoreintrag und lassen ihn bis dahin sein Ding machen
SemaphoreSlim.CurrentCount == amountOfSubscribers
(auch bekannt als: alle Plätze wurden freigegeben).Dies würde Ihr Programm im Wesentlichen blockieren, bis alle Event-Abonnenten fertig sind.
Möglicherweise möchten Sie auch eine Veranstaltung à la
GameShutDownFinished
für Ihre Abonnenten bereitstellen , die sie anrufen müssen, wenn sie mit der Aufgabe am Ende des Spiels fertig sind. In Kombination mit derSemaphoreSlim.Release(int)
Überladung können Sie jetzt alle Semaphoreinträge löschen und einfachSemaphore.Wait()
zum Blockieren des Threads verwenden. Anstatt zu überprüfen, ob alle Einträge gelöscht wurden oder nicht, warten Sie jetzt, bis ein Platz freigegeben wurde (es sollte jedoch nur einen Moment geben, in dem alle Plätze gleichzeitig freigegeben werden).quelle
Shutdown
hängt die Methode davon ab, welches Semaphor in den Handlern freigegeben wird, oder sollte ich einen Rückruf als Ereignisargument bereitstellen?RegisterShutdownCallback(Func<Task> callback)
, den Listener aufrufen, um einen erwarteten Rückruf zu registrieren. WennShutdown
dann aufgerufen wird, durchlaufe ich alle registrierten Rückrufe. Es fühlt sich nicht so gut an wie Eventing, aber es ist eine mögliche LösungIch weiß, dass die Operation speziell nach der Verwendung von Async und Aufgaben gefragt hat, aber hier ist eine Alternative, die bedeutet, dass die Handler keinen Wert zurückgeben müssen. Der Code basiert auf dem Beispiel von Peter Duniho. Zuerst die äquivalente Klasse A (etwas passend zusammengedrückt): -
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 zeigen ...
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 für jemanden nützlich ist.
quelle
Wenn Sie auf einen Standard-.net-Ereignishandler warten müssen, können Sie dies nicht tun, da dies der Fall ist
void
.Sie können jedoch ein asynchrones Ereignissystem erstellen, um dies zu handhaben:
public delegate Task AsyncEventHandler(AsyncEventArgs e); public class AsyncEventArgs : System.EventArgs { public bool Handled { get; set; } } public class AsyncEvent { private string name; private List<AsyncEventHandler> handlers; private Action<string, Exception> errorHandler; public AsyncEvent(string name, Action<string, Exception> errorHandler) { this.name = name; this.handlers = new List<AsyncEventHandler>(); this.errorHandler = errorHandler; } public void Register(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this.handlers) this.handlers.Add(handler); } public void Unregister(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this.handlers) this.handlers.Remove(handler); } public IReadOnlyList<AsyncEventHandler> Handlers { get { var temp = default(AsyncEventHandler[]); lock (this.handlers) temp = this.handlers.ToArray(); return temp.ToList().AsReadOnly(); } } public async Task InvokeAsync() { var ev = new AsyncEventArgs(); var exceptions = new List<Exception>(); foreach (var handler in this.Handlers) { try { await handler(ev).ConfigureAwait(false); if (ev.Handled) break; } catch(Exception ex) { exceptions.Add(ex); } } if (exceptions.Any()) this.errorHandler?.Invoke(this.name, new AggregateException(exceptions)); } }
Und Sie können jetzt Ihre asynchronen Ereignisse deklarieren:
public class MyGame { private AsyncEvent _gameShuttingDown; public event AsyncEventHandler GameShuttingDown { add => this._gameShuttingDown.Register(value); remove => this._gameShuttingDown.Unregister(value); } void ErrorHandler(string name, Exception ex) { // handle event error. } public MyGame() { this._gameShuttingDown = new AsyncEvent("GAME_SHUTTING_DOWN", this.ErrorHandler);. } }
Rufen Sie Ihr asynchrones Ereignis mit folgenden Funktionen auf:
internal async Task NotifyGameShuttingDownAsync() { await this._gameShuttingDown.InvokeAsync().ConfigureAwait(false); }
Generische Version:
public delegate Task AsyncEventHandler<in T>(T e) where T : AsyncEventArgs; public class AsyncEvent<T> where T : AsyncEventArgs { private string name; private List<AsyncEventHandler<T>> handlers; private Action<string, Exception> errorHandler; public AsyncEvent(string name, Action<string, Exception> errorHandler) { this.name = name; this.handlers = new List<AsyncEventHandler<T>>(); this.errorHandler = errorHandler; } public void Register(AsyncEventHandler<T> handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this.handlers) this.handlers.Add(handler); } public void Unregister(AsyncEventHandler<T> handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this.handlers) this.handlers.Remove(handler); } public IReadOnlyList<AsyncEventHandler<T>> Handlers { get { var temp = default(AsyncEventHandler<T>[]); lock (this.handlers) temp = this.handlers.ToArray(); return temp.ToList().AsReadOnly(); } } public async Task InvokeAsync(T ev) { var exceptions = new List<Exception>(); foreach (var handler in this.Handlers) { try { await handler(ev).ConfigureAwait(false); if (ev.Handled) break; } catch (Exception ex) { exceptions.Add(ex); } } if (exceptions.Any()) this.errorHandler?.Invoke(this.name, new AggregateException(exceptions)); } }
quelle