¿Cómo espero eventos en C #?

Estoy creando una clase que tiene una serie de eventos, uno de ellos GameShuttingDown . Cuando se activa este evento, necesito invocar el controlador de eventos. El objective de este evento es notificar a los usuarios que el juego se está cerrando y que necesitan guardar sus datos. Los rescates son agotables y los eventos no. Entonces, cuando se llama al controlador, el juego se apaga antes de que los manejadores esperados puedan completarlo.

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

Registro de evento

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

Entiendo que la firma de eventos es void EventName y, por lo tanto, hacerlo asincrónico es básicamente fuego y olvídate. Mi motor hace un uso intensivo de eventos para notificar a los desarrolladores de terceros (y a múltiples componentes internos) que se están produciendo eventos dentro del motor y permitirles reactjsr ante ellos.

¿Hay una buena ruta para reemplazar el evento con algo asincrónico que pueda usar? No estoy seguro si debería usar BeginShutdownGame y EndShutdownGame con devoluciones de llamada, pero eso es un problema porque solo la fuente que llama puede pasar una callback, y no cualquier cosa de terceros que se conecte al motor, que es lo que estoy recibiendo. con eventos. Si el servidor llama a game.ShutdownGame() , no hay forma de que los complementos de motor u otros componentes dentro del motor pasen sus devoluciones de llamada, a menos que conecte algún tipo de método de registro, manteniendo un conjunto de devoluciones de llamadas.

Cualquier consejo sobre cuál es la ruta preferida / recomendada para bajar con esto sería muy apreciada. He mirado alrededor y, en general, lo que he visto es utilizar el enfoque Begin / End, que no creo que satisfaga lo que quiero hacer.

Editar

Otra opción que estoy considerando es utilizar un método de registro, que toma una callback a la espera. Repito todas las devoluciones de llamada, tomo su Tarea y espero con 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); } 

Personalmente, creo que tener controladores de eventos async puede no ser la mejor opción de diseño, y la razón principal es el problema que estás teniendo. Con los controladores síncronos, es trivial saber cuándo se completan.

Dicho esto, si por alguna razón debe o al menos está fuertemente obligado a seguir con este diseño, puede hacerlo de una manera amigable.

Su idea de registrar manejadores y await es buena. Sin embargo, sugiero seguir con el paradigma de eventos existente, ya que mantendrá la expresividad de los eventos en su código. Lo principal es que debe desviarse del tipo de delegado estándar basado en EventHandler y usar un tipo de delegado que devuelve una Task para que pueda await los manejadores.

Aquí hay un ejemplo simple que ilustra lo que quiero decir:

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

El método OnShutdown() , después de hacer el estándar “obtener copia local de la instancia delegada del evento”, primero invoca a todos los manejadores y luego espera todas las Tasks devueltas (habiéndolas guardado en una matriz local cuando se invocan los manejadores) .

Aquí hay un breve progtwig de consola que ilustra el uso:

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

Habiendo pasado por este ejemplo, ahora me pregunto si no podría haber habido una manera para que C # resum esto un poco. Tal vez hubiera sido un cambio demasiado complicado, pero la combinación actual de los manejadores de eventos de devolución de void estilo antiguo y la nueva característica async / await parece un poco incómoda. Lo anterior funciona (y funciona bien, en mi humilde opinión), pero hubiera sido bueno tener un mejor CLR y / o soporte de idioma para el escenario (es decir, poder esperar a un delegado de multidifusión y hacer que el comstackdor C # lo convierta en una llamada a 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); } } } 

ejemplo:

  public event EventHandler 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); } }); }); } 

Nota: Esto es asincrónico, por lo que el controlador de eventos puede comprometer el subproceso de interfaz de usuario. El controlador de eventos (suscriptor) no debe hacer ningún trabajo de UI. No tendría mucho sentido de lo contrario.

  1. declare su evento en su proveedor de eventos:

    evento público EventHandler DoSomething;

  2. Invoque el evento de su proveedor:

    DoSomething.InvokeAsync (nuevo MyEventArgs (), esto, ar => {callback llamado cuando finaliza (¡sincroniza la IU cuando sea necesario aquí!)}, Nulo);

  3. suscribe el evento por cliente como lo harías normalmente

Es cierto, los eventos son inherentemente no esperables, por lo que tendrás que evitarlo.

Una solución que he usado en el pasado es usar un semáforo para esperar a que se publiquen todas las entradas en él. En mi situación, solo tuve un evento suscrito, así que pude codificarlo como new SemaphoreSlim(0, 1) pero en su caso es posible que desee anular el getter / setter para su evento y mantener un contador de la cantidad de suscriptores allí para que pueda puede establecer dinámicamente la cantidad máxima de hilos simultáneos.

Luego pasas una entrada de semáforo a cada uno de los suscriptores y les dejas hacer lo suyo hasta que SemaphoreSlim.CurrentCount == amountOfSubscribers (también conocido como: todos los espacios se hayan liberado).

Esto esencialmente bloquearía su progtwig hasta que todos los suscriptores del evento hayan terminado.

También es posible que desee considerar la posibilidad de ofrecer un evento al estilo GameShutDownFinished para sus suscriptores, al que tienen que llamar cuando hayan terminado con su tarea de fin de juego. Combinado con la sobrecarga SemaphoreSlim.Release(int) , ahora puede borrar todas las entradas del semáforo y simplemente usar Semaphore.Wait() para bloquear el hilo. En lugar de tener que comprobar si todas las entradas se han borrado, espere hasta que se haya liberado un punto (pero solo debería haber un momento en que todos los puntos se liberen a la vez).

Sé que la operación estaba preguntando específicamente sobre el uso de asincronización y tareas para esto, pero aquí hay una alternativa que significa que los manejadores no necesitan devolver un valor. El código está basado en el ejemplo de Peter Duniho. Primero, la clase equivalente A (aplastada un poco para caber): –

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

Una aplicación de consola simple para mostrar su uso …

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

Espero que esto sea útil para alguien.

    Intereting Posts