Jak czekać na pojedyncze zdarzenie w C #, z limitem czasu i anulowaniem

Zatem moim wymaganiem jest, aby moja funkcja czekała na pierwszą instancję ievent Action<T> pochodzący z innej klasy i innego wątku i obsługujący go w moim wątku, pozwalając na przerwanie oczekiwania przez timeout lubCancellationToken.

Chcę utworzyć ogólną funkcję, której mogę użyć ponownie. Udało mi się stworzyć kilka opcji, które robią (myślę) to, czego potrzebuję, ale obie wydają się bardziej skomplikowane niż sobie wyobrażam.

Stosowanie

Dla jasności, przykładowe użycie tej funkcji wyglądałoby tak, gdzieserialDevice wypluwa wydarzenia w osobnym wątku:

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

Ta opcja nie jest zła, aleDispose obsługaManualResetEventSlim jest bardziej niechlujny niż mogłoby się wydawać. Daje ReSharper pasuje, że mam dostęp do zmodyfikowanych / usuniętych rzeczy w zamknięciu i naprawdę trudno jest go śledzić, więc nie jestem nawet pewien, czy jest poprawny. Może brakuje mi czegoś, co mogłoby to oczyścić, co byłoby moją preferencją, ale nie widzę tego odręcznie. Oto kod.

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;
    }
}
Opcja 2 - odpytywanie bezWaitHandle

TheWaitForSingleEvent funkcja tutaj jest znacznie czystsza. Jestem w stanie użyćConcurrentQueue i dlatego nawet nie potrzebuję blokady. Ale nie podoba mi się funkcja odpytywaniaSleepi nie widzę tego w ten sposób. Chciałbym przejśćWaitHandle zamiastFunc<bool> posprzątaćSleep, ale za drugim razem mam całośćDispose bałagan ponownie posprzątać.

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

Nie dbam o żadne z tych rozwiązań ani nie jestem w 100% pewien, że któreś z nich są w 100% poprawne. Czy jedno z tych rozwiązań jest lepsze od drugiego (idiomatyczność, wydajność itp.), Czy też istnieje łatwiejszy sposób lub wbudowana funkcja, aby sprostać temu, co muszę zrobić tutaj?

Aktualizacja: jak dotąd najlepsza odpowiedź

ModyfikacjaTaskCompletionSource rozwiązanie poniżej. Brak długich zamknięć, zamków lub czegokolwiek innego. Wydaje się całkiem proste. Jakieś błędy tutaj?

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;
}
Aktualizacja 2: Kolejne świetne rozwiązanie

Okazuje się, żeBlockingCollection działa tak jakConcurrentQueue ale ma także metody akceptujące limit czasu i żeton anulowania. Jedną z zalet tego rozwiązania jest możliwość aktualizacjiWaitForNEvents dość łatwe:

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

questionAnswers(2)

yourAnswerToTheQuestion