Warten auf ein einzelnes Ereignis in C # mit Zeitüberschreitung und Abbruch
Meine Anforderung ist es also, meine Funktion auf die erste Instanz warten zu lassenevent Action<T>
Komme aus einer anderen Klasse und einem anderen Thread und bearbeite es in meinem Thread, so dass das Warten entweder durch timeout oder durch timeout unterbrochen werden kannCancellationToken
.
Ich möchte eine generische Funktion erstellen, die ich wiederverwenden kann. Ich habe es geschafft, ein paar Optionen zu erstellen, die (meiner Meinung nach) genau das tun, was ich brauche, aber beide scheinen komplizierter zu sein, als ich es mir vorgestellt hätte.
VerwendungszweckUm es klar zu machen, würde eine Beispielanwendung dieser Funktion wie folgt aussehenserialDevice
spuckt Ereignisse auf einen separaten Thread aus:
var eventOccurred = Helper.WaitForSingleEvent<StatusPacket>(
cancellationToken,
statusPacket => OnStatusPacketReceived(statusPacket),
a => serialDevice.StatusPacketReceived += a,
a => serialDevice.StatusPacketReceived -= a,
5000,
() => serialDevice.RequestStatusPacket());
Option 1 - ManualResetEventSlimDiese Option ist nicht schlecht, aber dieDispose
Umgang mit demManualResetEventSlim
ist unordentlicher als es scheint, wie es sein sollte. Es gibt ReSharper Passungen, dass ich auf geänderte / entsorgte Dinge innerhalb des Verschlusses zugreife, und es ist wirklich schwer zu folgen, so dass ich nicht einmal sicher bin, ob es korrekt ist. Vielleicht fehlt mir etwas, um das aufzuräumen, was ich bevorzugen würde, 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 - Abfrage ohne aWaitHandle
DasWaitForSingleEvent
funktion hier ist viel sauberer. Ich kann es benutzenConcurrentQueue
und brauchen nicht einmal ein Schloss. Aber die Polling-Funktion gefällt mir einfach nichtSleep
und ich sehe bei diesem Ansatz keinen Ausweg. Ich würde gerne eineWaitHandle
anstelle einerFunc<bool>
AufräumenSleep
, aber beim zweiten Mal habe ich das ganzeDispose
Chaos, um 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 FrageIch mag keine dieser Lösungen besonders, und ich bin mir auch nicht hundertprozentig sicher, dass beide Lösungen hundertprozentig korrekt sind. Ist eine dieser Lösungen besser als die andere (Idiomatizität, Effizienz usw.) oder gibt es eine einfachere Möglichkeit oder integrierte Funktion, um das zu erfüllen, was ich hier tun muss?
Update: Beste Antwort bisherEine Modifikation derTaskCompletionSource
Lösung unten. Keine langen Verschlüsse, Schlösser oder irgendetwas erforderlich. Scheint ziemlich einfach zu sein. 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 weitere großartige LösungStellt sich heraus, dassBlockingCollection
funktioniert genauso wieConcurrentQueue
Es gibt aber auch Methoden, die ein Timeout- und Abbruchtoken akzeptieren. Eine schöne Sache an dieser Lösung ist, dass sie aktualisiert werden kann, um eineWaitForNEvents
ziemlich einfach:
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();
}
}