tipos que programacion orientada objetos lista eventos evento event desuscribirse definir declarar c# multithreading events asynchronous dispose

c# - lista - que es un evento en programacion orientada a objetos



Cómo esperar un solo evento en C#, con tiempo de espera y cancelación (2)

Por lo tanto, mi requisito es que mi función espere la primera instancia de un event Action<T> proveniente de otra clase y otro subproceso, y manejarlo en mi hilo, permitiendo que la espera sea interrumpida por tiempo de espera o CancellationToken .

Quiero crear una función genérica que pueda reutilizar. Me las arreglé para crear un par de opciones que hacen (creo) lo que necesito, pero ambas parecen más complicadas de lo que me imagino que deberían ser.

Uso

Para que quede claro, un ejemplo de uso de esta función se vería así, donde serialDevice está escupiendo eventos en un hilo separado:

var eventOccurred = Helper.WaitForSingleEvent<StatusPacket>( cancellationToken, statusPacket => OnStatusPacketReceived(statusPacket), a => serialDevice.StatusPacketReceived += a, a => serialDevice.StatusPacketReceived -= a, 5000, () => serialDevice.RequestStatusPacket());

Opción 1: ManualResetEventSlim

Esta opción no es mala, pero el manejo de Dispose de ManualResetEventSlim es más complicado de lo que parece. Da a los ajustes ReSharper que estoy accediendo a cosas modificadas / dispuestas dentro del cierre, y es realmente difícil de seguir, así que ni siquiera estoy seguro de que sea correcto. Tal vez falte algo que pueda limpiar esto, que sería mi preferencia, pero no lo veo de repente. Aquí está el código.

public static bool WaitForSingleEvent<TEvent>(this CancellationToken token, Action<TEvent> handler, Action<Action<TEvent>> subscribe, Action<Action<TEvent>> unsubscribe, int msTimeout, Action initializer = null) { var eventOccurred = false; var eventResult = default(TEvent); var o = new object(); var slim = new ManualResetEventSlim(); Action<TEvent> setResult = result => { lock (o) // ensures we get the first event only { if (!eventOccurred) { eventResult = result; eventOccurred = true; // ReSharper disable AccessToModifiedClosure // ReSharper disable AccessToDisposedClosure if (slim != null) { slim.Set(); } // ReSharper restore AccessToDisposedClosure // ReSharper restore AccessToModifiedClosure } } }; subscribe(setResult); try { if (initializer != null) { initializer(); } slim.Wait(msTimeout, token); } finally // ensures unsubscription in case of exception { unsubscribe(setResult); lock(o) // ensure we don''t access slim { slim.Dispose(); slim = null; } } lock (o) // ensures our variables don''t get changed in middle of things { if (eventOccurred) { handler(eventResult); } return eventOccurred; } }

Opción 2: sondeo sin WaitHandle

La función WaitForSingleEvent aquí es mucho más limpia. Soy capaz de usar ConcurrentQueue y, por lo tanto, ni siquiera necesito un bloqueo. Pero simplemente no me gusta la función de sondeo Sleep , y no veo ninguna manera de evitarlo con este enfoque. Me gustaría pasar un WaitHandle lugar de un Func<bool> para limpiar el Sleep , pero el segundo en que lo haga tengo todo el lío de Dispose para limpiar nuevamente.

public static bool WaitForSingleEvent<TEvent>(this CancellationToken token, Action<TEvent> handler, Action<Action<TEvent>> subscribe, Action<Action<TEvent>> unsubscribe, int msTimeout, Action initializer = null) { var q = new ConcurrentQueue<TEvent>(); subscribe(q.Enqueue); try { if (initializer != null) { initializer(); } token.Sleep(msTimeout, () => !q.IsEmpty); } finally // ensures unsubscription in case of exception { unsubscribe(q.Enqueue); } TEvent eventResult; var eventOccurred = q.TryDequeue(out eventResult); if (eventOccurred) { handler(eventResult); } return eventOccurred; } public static void Sleep(this CancellationToken token, int ms, Func<bool> exitCondition) { var start = DateTime.Now; while ((DateTime.Now - start).TotalMilliseconds < ms && !exitCondition()) { token.ThrowIfCancellationRequested(); Thread.Sleep(1); } }

La pregunta

No me preocupo especialmente por ninguna de estas soluciones, ni estoy 100% seguro de que ninguna de ellas sea 100% correcta. ¿Alguna de estas soluciones es mejor que la otra (idiomaticidad, eficiencia, etc.) o existe una forma más fácil o una función integrada para cumplir con lo que necesito hacer aquí?

Actualización: la mejor respuesta hasta ahora

Una modificación de la solución TaskCompletionSource continuación. No se requieren cierres largos, cerraduras, ni nada. Parece bastante sencillo. ¿Algún error aquí?

public static bool WaitForSingleEvent<TEvent>(this CancellationToken token, Action<TEvent> onEvent, Action<Action<TEvent>> subscribe, Action<Action<TEvent>> unsubscribe, int msTimeout, Action initializer = null) { var tcs = new TaskCompletionSource<TEvent>(); Action<TEvent> handler = result => tcs.TrySetResult(result); var task = tcs.Task; subscribe(handler); try { if (initializer != null) { initializer(); } task.Wait(msTimeout, token); } finally { unsubscribe(handler); // Do not dispose task http://blogs.msdn.com/b/pfxteam/archive/2012/03/25/10287435.aspx } if (task.Status == TaskStatus.RanToCompletion) { onEvent(task.Result); return true; } return false; }

Actualización 2: Otra gran solución

Resulta que BlockingCollection funciona como ConcurrentQueue pero también tiene métodos que aceptan un token de tiempo de espera y cancelación. Una cosa buena de esta solución es que puede actualizarse para hacer que WaitForNEvents bastante fácil:

public static bool WaitForSingleEvent<TEvent>(this CancellationToken token, Action<TEvent> handler, Action<Action<TEvent>> subscribe, Action<Action<TEvent>> unsubscribe, int msTimeout, Action initializer = null) { var q = new BlockingCollection<TEvent>(); Action<TEvent> add = item => q.TryAdd(item); subscribe(add); try { if (initializer != null) { initializer(); } TEvent eventResult; if (q.TryTake(out eventResult, msTimeout, token)) { handler(eventResult); return true; } return false; } finally { unsubscribe(add); q.Dispose(); } }


Puede usar TaskCompletetionSource para crear una Task que puede marcar como completada o cancelada. Aquí hay una posible implementación para un evento específico:

public Task WaitFirstMyEvent(Foo target, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource<object>(); Action handler = null; var registration = cancellationToken.Register(() => { target.MyEvent -= handler; tcs.TrySetCanceled(); }); handler = () => { target.MyEvent -= handler; registration.Dispose(); tcs.TrySetResult(null); }; target.MyEvent += handler; return tcs.Task; }

En C # 5 puedes usarlo así:

private async Task MyMethod() { ... await WaitFirstMyEvent(foo, cancellationToken); ... }

Si desea esperar el evento de forma síncrona, también puede usar el método de Wait :

private void MyMethod() { ... WaitFirstMyEvent(foo, cancellationToken).Wait(); ... }

Aquí hay una versión más genérica, pero aún funciona solo para eventos con firma de Action :

public Task WaitFirstEvent( Action<Action> subscribe, Action<Action> unsubscribe, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource<object>(); Action handler = null; var registration = cancellationToken.Register(() => { unsubscribe(handler); tcs.TrySetCanceled(); }); handler = () => { unsubscribe(handler); registration.Dispose(); tcs.TrySetResult(null); }; subscribe(handler); return tcs.Task; }

Puedes usarlo así:

await WaitFirstEvent( handler => foo.MyEvent += handler, handler => foo.MyEvent -= handler, cancellationToken);

Si desea que funcione con otras firmas de eventos (por ejemplo, EventHandler ), tendrá que crear sobrecargas separadas. No creo que haya una manera fácil de hacer que funcione para cualquier firma, especialmente porque la cantidad de parámetros no siempre es la misma.


Puede usar Rx para convertir el evento en un observable, luego en una tarea y, finalmente, esperar en esa tarea con su token / timeout.

Una de las ventajas que esto tiene sobre cualquiera de las soluciones existentes es que llama a unsubscribe la unsubscribe en el hilo del evento, lo que garantiza que su manejador no será llamado dos veces. (En su primera solución, trabaja en tcs.TrySetResult lugar de tcs.SetResult , pero siempre es bueno deshacerse de un "TryDoSomething" y simplemente asegurarse de que DoSomething siempre funcione).

Otra ventaja es la simplicidad del código. Es esencialmente una línea. Así que ni siquiera necesitas una función independiente. Puede integrarlo para que quede más claro qué hace exactamente su código, y puede hacer variaciones en el tema sin necesidad de un montón de parámetros opcionales (como su initializer opcional, o permitir la espera de N eventos, o los tiempos de espera / cancelación anteriores en las instancias donde no sean necesarios). Y tendría tanto el valor devuelto de bool como el result real en el alcance cuando esté terminado, si es que es útil.

using System.Reactive.Linq; using System.Reactive.Threading.Tasks; ... public static bool WaitForSingleEvent<TEvent>(this CancellationToken token, Action<TEvent> onEvent, Action<Action<TEvent>> subscribe, Action<Action<TEvent>> unsubscribe, int msTimeout, Action initializer = null) { var task = Observable.FromEvent(subscribe, unsubscribe).FirstAsync().ToTask(); if (initializer != null) { initializer(); } try { var finished = task.Wait(msTimeout, token); if (finished) onEvent(task.Result); return finished; } catch (OperationCanceledException) { return false; } }