Come aspettare un singolo evento in C #, con timeout e cancellazione

Quindi il mio requisito è quello di avere la mia funzione in attesa della prima istanza di un event Action proveniente da un’altra class e un altro thread, e gestirlo sul mio thread, permettendo l’attesa di essere interrotta dal timeout o da CancellationToken .

Voglio creare una funzione generica che posso riutilizzare. Sono riuscito a creare un paio di opzioni che fanno (credo) ciò di cui ho bisogno, ma sembrano entrambe più complicate di quanto immaginerei dovrebbe essere.

uso

Per essere chiari, un uso di esempio di questa funzione sarebbe simile a questo, dove serialDevice sta sputando gli eventi su un thread separato:

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

Opzione 1-ManualResetEventSlim

Questa opzione non è male, ma la gestione del Dispose di ManualResetEventSlim è più ManualResetEventSlim di quanto possa sembrare. Dà a ReSharper la convinzione che sto accedendo a cose modificate / disposte all’interno della chiusura, ed è davvero difficile da seguire quindi non sono nemmeno sicuro che sia corretto. Forse c’è qualcosa che mi manca che può ripulire, che sarebbe la mia preferenza, ma non la vedo subito. Ecco il codice.

 public static bool WaitForSingleEvent(this CancellationToken token, Action handler, Action<Action> subscribe, Action<Action> unsubscribe, int msTimeout, Action initializer = null) { var eventOccurred = false; var eventResult = default(TEvent); var o = new object(); var slim = new ManualResetEventSlim(); Action 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; } } 

Opzione 2-polling senza WaitHandle

La funzione WaitForSingleEvent qui è molto più pulita. Sono in grado di utilizzare ConcurrentQueue e quindi non ho nemmeno bisogno di un blocco. Ma non mi piace la funzione di polling Sleep , e non vedo alcun modo di aggirarlo con questo approccio. Mi piacerebbe passare un WaitHandle invece di un Func per ripulire Sleep , ma nel secondo faccio che ho tutto il disordine di Dispose per ripulire.

 public static bool WaitForSingleEvent(this CancellationToken token, Action handler, Action<Action> subscribe, Action<Action> unsubscribe, int msTimeout, Action initializer = null) { var q = new ConcurrentQueue(); 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 exitCondition) { var start = DateTime.Now; while ((DateTime.Now - start).TotalMilliseconds < ms && !exitCondition()) { token.ThrowIfCancellationRequested(); Thread.Sleep(1); } } 

La domanda

Non mi interessa particolarmente nessuna di queste soluzioni, né sono sicuro al 100% che entrambe siano corrette al 100%. È una di queste soluzioni migliore dell’altra (idiomaticità, efficienza, ecc.) Oppure esiste un modo più semplice o una funzione incorporata per soddisfare ciò che devo fare qui?

Aggiornamento: la migliore risposta finora

Una modifica della soluzione TaskCompletionSource seguito. Non ci sono chiusure lunghe, serrature o altro. Sembra abbastanza semplice. Qualche errore qui?

 public static bool WaitForSingleEvent(this CancellationToken token, Action onEvent, Action<Action> subscribe, Action<Action> unsubscribe, int msTimeout, Action initializer = null) { var tcs = new TaskCompletionSource(); Action 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; } 

Aggiornamento 2: un’altra grande soluzione

Risulta che BlockingCollection funziona proprio come ConcurrentQueue ma ha anche metodi che accettano un token di timeout e cancellazione. Una cosa bella di questa soluzione è che può essere aggiornato per rendere WaitForNEvents abbastanza facilmente:

 public static bool WaitForSingleEvent(this CancellationToken token, Action handler, Action<Action> subscribe, Action<Action> unsubscribe, int msTimeout, Action initializer = null) { var q = new BlockingCollection(); Action 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(); } } 

È ansible utilizzare Rx per convertire l’evento in osservabile, quindi in un’attività e infine attendere tale attività con il token / timeout.

Un vantaggio che ha su qualsiasi delle soluzioni esistenti, è che si unsubscribe l’ unsubscribe della discussione sul thread dell’evento, assicurandosi che il gestore non venga chiamato due volte. (Nella tua prima soluzione, tcs.TrySetResult questo tcs.TrySetResult con tcs.TrySetResult invece di tcs.SetResult , ma è sempre bello sbarazzarsi di un “TryDoSomething” e assicurarsi semplicemente che DoSomething funzioni sempre).

Un altro vantaggio è la semplicità del codice. È essenzialmente una linea. Quindi non hai nemmeno particolarmente bisogno di una funzione indipendente. Puoi inline in modo che sia più chiaro cosa fa esattamente il tuo codice, e puoi apportare variazioni sul tema senza bisogno di un sacco di parametri opzionali (come l’ initializer opzionale, o consentire l’attesa su N eventi, o rinunciare a timeout / cancellazione nelle istanze dove non sono necessari). E avresti sia il valore restituito da bool che il result effettivo in ambito quando è finito, se questo è utile a tutti.

 using System.Reactive.Linq; using System.Reactive.Threading.Tasks; ... public static bool WaitForSingleEvent(this CancellationToken token, Action onEvent, Action> subscribe, Action> 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; } } 

È ansible utilizzare TaskCompletetionSource per creare un’attività che è ansible contrassegnare come completata o annullata. Ecco una ansible implementazione per un evento specifico:

 public Task WaitFirstMyEvent(Foo target, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(); 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; } 

In C # 5 puoi usarlo in questo modo:

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

Se si desidera attendere l’evento in modo sincrono, è anche ansible utilizzare il metodo Wait :

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

Ecco una versione più generica, ma funziona ancora solo per gli eventi con firma Action :

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

Puoi usarlo in questo modo:

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

Se si desidera che funzioni con altre firme di eventi (ad esempio EventHandler ), sarà necessario creare overload separati. Non penso che ci sia un modo semplice per farlo funzionare per qualsiasi firma, soprattutto perché il numero di parametri non è sempre lo stesso.