Как ждать одного события в C#, с таймаутом и отменой

поэтому мое требование состоит в том, чтобы моя функция ждала первого экземпляра event Action<T> приходит из другого класса и другого потока и обрабатывает его в моем потоке, позволяя ожиданию прерываться либо таймаутом, либо CancellationToken.

я хочу создать общую функцию, которую я могу повторно использовать. Мне удалось создать пару вариантов, которые делают (я думаю) то, что мне нужно, но оба кажутся более сложными, чем я предполагал.

использование

просто быть ясно, пример использования этой функции будет выглядеть следующим образом, где serialDevice выплевывает события в отдельный поток:

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

Вариант 1-ManualResetEventSlim

этот вариант не плох, но Dispose обработка ManualResetEventSlim - это сложнее, чем кажется, что это должно быть. Это дает ReSharper fits, что я получаю доступ к измененным / размещенным вещам в закрытии, и действительно трудно следовать, поэтому я даже не уверен, что это правильно. Может быть, я что-то отсутствие этого может очистить это, что было бы моим предпочтением, но я не вижу этого сразу. Вот код.

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

Вариант 2-опрос без WaitHandle

на WaitForSingleEvent функция здесь намного чище. Я могу использовать ConcurrentQueue и при этом даже не нужен замок. Но мне просто не нравится функция опроса Sleep, и я не вижу никакого способа обойти это с помощью этого подхода. Я хотел бы пройти в WaitHandle вместо Func<bool> очистить Sleep, но во-вторых, у меня есть все Dispose бардак разгребать.

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

вопрос

я не особенно забочусь о любом из этих решений, и я не уверен на 100%, что любой из них на 100% правильный. Является ли одно из этих решений лучше другого (идиоматичность, эффективность и т. д.), Или есть более простой способ или встроенная функция для удовлетворения того, что мне нужно сделать здесь?

Update: лучший ответ до сих пор

модификация TaskCompletionSource Решение ниже. Никаких затворов, замков или чего-либо еще. Выглядит довольно просто. Есть ошибки?

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: Еще одно отличное решение

получается, что BlockingCollection работает так же, как ConcurrentQueue но также имеет методы принятия тайм-аут и токен отмены. Одна хорошая вещь об этом решении заключается в том, что его можно обновить, чтобы сделать WaitForNEvents довольно легко:

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

2 ответов


вы можете использовать Rx для преобразования события в наблюдаемое, затем в задачу и, наконец, дождаться этой задачи с вашим токеном/таймаутом.

одним из преимуществ над любой из существующих решений является то, что он называет unsubscribe в потоке события,сделать что ваш обработчик не будет вызван дважды. (В своем первом решении вы обходите это с помощью tcs.TrySetResult вместо tcs.SetResult, но всегда приятно избавиться от "TryDoSomething" и просто обеспечить DoSomething всегда завод.)

еще одним преимуществом является простота кода. По сути, это одна линия. Таким образом, вам даже не особенно нужна независимая функция. Вы можете встроить его, чтобы было более ясно, что именно делает ваш код, и вы можете вносить изменения в тему без необходимости тонны дополнительных параметров (например, ваш дополнительный initializer, или разрешить ожидание N событий или предшествующие таймауты/отмена в случаях, когда они не нужны). А вы бы как bool вернуться Вэл!--10-->и фактический result в области, когда он будет завершен, если это вообще полезно.

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

можно использовать TaskCompletetionSource создать Task что вы можете отметить как завершено или отменено. Вот возможная реализация для конкретного события:

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

в C# 5, Вы можете использовать его как это:

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

если вы хотите дождаться события синхронно, вы можете также использовать Wait способ:

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

вот более общая версия, но она по-прежнему работает только для событий с Action подпись:

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

вы можете использовать его вот так:

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

если вы хотите, чтобы он работал с другими сигнатурами событий (например,EventHandler), вам придется создавать отдельные перегрузки. Я не думаю, что есть простой способ заставить его работать для любой подписи, тем более, что количество параметров не всегда одинаково.