2013-07-14 12 views
17

So ein event Action<T> von einer anderen Klasse meiner Anforderung ist meine Funktion Warten auf die erste Instanz zu haben und eine anderen Thread kommt, und behandelt es auf meinem Thread, die Wartezeit erlaubt durch Timeout oder CancellationToken unterbrochen werden.Wie für ein einzelnes Ereignis in C# warten, mit Timeout und Löschung

Ich möchte eine generische Funktion erstellen, ich wiederverwenden können. Es ist mir gelungen, ein paar Optionen zu schaffen, die (denke ich) das tun, was ich brauche, aber beide scheinen komplizierter zu sein, als ich mir vorstellen kann.

Nutzungs

Nur klar zu sein, eine Probe die Verwendung dieser Funktion wie folgt aussehen würde, wo serialDevice spuckt Ereignisse auf einem separaten Thread:

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

Option 1-ManualResetEventSlim

Diese Option ist nicht schlecht, aber die Dispose Handhabung der ManualResetEventSlim ist chaotischer, als es scheint, wie es sein sollte. Es gibt ReSharper passt, dass ich auf modifizierte/entsorgte Dinge innerhalb der Schließung zugreifen, und es ist wirklich schwer zu folgen, so dass ich nicht einmal sicher bin, dass es korrekt ist. Vielleicht gibt es etwas, das ich vermisse, das das aufräumen kann, was meine Vorliebe wäre, aber ich sehe es nicht ohne weiteres. Hier ist der Code.

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

Option 2-Polling ohne WaitHandle

Die WaitForSingleEvent Funktion hier ist viel sauberer. Ich kann ConcurrentQueue verwenden und brauche daher nicht einmal eine Sperre. Aber ich mag einfach nicht die Polling-Funktion Sleep, und ich sehe es mit diesem Ansatz nicht herum. Ich möchte in einem WaitHandle passieren anstelle eines Func<bool> aufzuräumen Sleep, aber die zweite ich, dass ich die ganze Dispose Chaos haben wieder aufzuräumen.

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

Die Frage

ich für eine dieser beiden Lösungen nicht besonders kümmern, noch bin ich 100% sicher, dass jeder von ihnen 100% korrekt sind. Ist entweder eine dieser Lösungen besser als die andere (Idiomatik, Effizienz usw.) oder gibt es einen einfacheren Weg oder eine eingebaute Funktion, um das zu erfüllen, was ich hier tun muss?

Update: Beste Antwort bisher

Eine Modifikation der TaskCompletionSource Lösung unten. Keine langen Verschlüsse, Schlösser oder irgendetwas benötigt. Scheint ziemlich einfach. Irgendwelche Fehler hier?

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

Update 2: Eine andere große Lösung

Es stellte sich heraus, dass BlockingCollection Werke ConcurrentQueue genau wie hat aber auch Methoden, um ein Timeout und Stornierungs Token zu akzeptieren. Eine nette Sache über diese Lösung ist, dass es aktualisiert werden kann, ein WaitForNEvents ziemlich leicht zu machen:

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

Es klingt, als ob Sie etwas wie 'AutoResetEvent' möchten. Hast du die Möglichkeit gesehen, es zu benutzen? –

+0

@KendallFrey Ja, das scheint mich in die gleiche 'Dispose'-Verwirrung zu bringen, die' ManualResetEventSlim' hat, oder hattest du einen Weg darum herum? – lobsterism

+0

Der Grund, dass Resharper sich beschwert, ist, weil es den Fluss nicht analysieren kann, da die Subscribe- und Abmeldeaktionen übergeben werden. Es kann aufhören, sich zu beklagen, wenn Sie das Ereignis (über Reflektion) übertrugen oder irgendwie den Fluss mehr verifizierbar machten. In jedem Fall halte ich persönlich die Responderwarnungen nicht hoch. –

Antwort

2

Sie können Rx verwenden, um das Ereignis in eine beobachtbare, dann in eine Aufgabe zu konvertieren, und schließlich auf diese Aufgabe mit Ihrem Token/Timeout warten.

Ein Vorteil dieser über eine der vorhandenen Lösungen hat, ist, dass es nennt unsubscribe auf das Gewinde des Ereignisses, gewährleisten, dass Ihr Handler nicht zweimal aufgerufen werden. (In deiner ersten Lösung arbeitest du um tcs.TrySetResult anstelle von tcs.SetResult herum, aber es ist immer schön, ein "TryDoSomething" loszuwerden und einfach sicherzustellen, dass DoSomething immer funktioniert).

Ein weiterer Vorteil ist die Einfachheit des Codes. Es ist im Wesentlichen eine Zeile. Sie brauchen also nicht einmal eine unabhängige Funktion. Sie können es inline einfügen, so dass es klarer ist, was genau Ihr Code tut, und Sie können Variationen über das Thema machen, ohne eine Tonne optionaler Parameter zu benötigen (wie Ihr optionales initializer, oder warten auf N Ereignisse oder vorzeitige Timeouts/Annullierungen in Instanzen wo sie nicht nötig sind). Und Sie hätten sowohl die bool Rückgabewerte als auch die die tatsächlichen result im Umfang, wenn es fertig ist, wenn das überhaupt nützlich ist.

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

Sie TaskCompletetionSource verwenden können Task zu erstellen, die Sie als abgeschlossen oder abgebrochen markieren können.Hier ist eine mögliche Implementierung für ein bestimmtes Ereignis:

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

In C# 5 Sie es wie folgt verwenden können:

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

Wenn Sie für das Ereignis synchron warten wollen, können Sie auch die Verwendung Wait Methode :

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

Hier ist eine generische Ausführung, aber es funktioniert mit Action Unterschrift für Veranstaltungen nur noch:

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

Sie können es wie folgt verwenden:

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

Wenn Sie es wollen, mit anderen Ereignis Signaturen arbeiten (z EventHandler), müssen Sie separate Überladungen erstellen. Ich glaube nicht, dass es einen einfachen Weg gibt, um für jede Signatur zu funktionieren, besonders da die Anzahl der Parameter nicht immer gleich ist.

+0

Ich habe der Frage ein Update hinzugefügt - eine mögliche Lösung anhand Ihrer Beispiele als Ausgangspunkt. Ich habe keine Erfahrung mit "TaskCompletionSource" oder wirklich "Task" im Allgemeinen. Sehen Sie irgendwelche eklatanten Fehler in der Lösung? (Es ist. Net 4.0 und eine Desktop-App, so dass der Thread mit "task.Wait" ist kein Problem.) – lobsterism

+0

@lob, gut, Ihr Code unterstützt keine Stornierung. Außerdem ist es nicht sinnvoll, den Status der Aufgabe zu testen: Wenn dieser Punkt erreicht wird, kann der Status nichts anderes als RanToCompletion sein, sonst hätte sich eine Ausnahme gebildet. –

+0

Hinweis: Ich benutze 'task.Wait 'Überladung, die ein Timeout und ein CancellationToken dauert. Es scheint den Job zu machen. Wenn das Token abgebrochen wird, wenn 'Wait' aufgerufen wird, dann wird' OperationCancellationException' ausgegeben und wenn es abläuft, bleibt der 'Status' 'TaskStatus.WaitingForActivation'. – lobsterism

Verwandte Themen