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.

Verwendungszweck

Um 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 - ManualResetEventSlim

Diese 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 nichtSleepund 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 Frage

Ich 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 bisher

Eine 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ösung

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

Antworten auf die Frage(2)

Ihre Antwort auf die Frage