如何在C#中等待单个事件,具有超时和取消

时间:2013-07-14 00:30:50

标签: c# multithreading events asynchronous dispose

所以我的要求是让我的函数等待来自另一个类和另一个线程的event Action<T>的第一个实例,并在我的线程上处理它,允许等待被超时或{{1 }}。

我想创建一个可以重用的泛型函数。我设法创建了一些(我认为)我需要的选项,但两者看起来都比我想象的要复杂得多。

用法

为了清楚起见,这个函数的示例使用看起来像这样,CancellationToken在一个单独的线程上吐出事件:

serialDevice

选项1-ManualResetEventSlim

这个选项并不错,但var eventOccurred = Helper.WaitForSingleEvent<StatusPacket>( cancellationToken, statusPacket => OnStatusPacketReceived(statusPacket), a => serialDevice.StatusPacketReceived += a, a => serialDevice.StatusPacketReceived -= a, 5000, () => serialDevice.RequestStatusPacket()); 处理Dispose比看起来应该更糟糕。它让ReSharper适合我在闭包内访问修改/处理的东西,而且真的很难遵循,所以我甚至不确定它是否正确。也许有一些我遗漏的东西可以清理它,这将是我的偏好,但我不会随便看到它。这是代码。

ManualResetEventSlim

选项2 - 没有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; } }

的投票

这里的WaitHandle功能更清晰。我可以使用WaitForSingleEvent,因此甚至不需要锁定。但我只是不喜欢轮询函数ConcurrentQueue,我不认为这种方法可以解决它。我想传递一个Sleep而不是WaitHandle来清理Func<bool>,但是第二个我这样做,我已经把整个Sleep一团糟打扫干净了再一次。

Dispose

问题

我并不特别关心这些解决方案中的任何一种,也不是100%确定其中任何一种都是100%正确的。这些解决方案中的任何一个比其他解决方案更好(惯用性,效率等),还是有更简单的方法或内置函数来满足我在这里需要做的事情?

更新:目前为止的最佳答案

以下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); } } 解决方案的修改。没有长的闭合,锁或任何需要的东西。看起来非常简单。这里有任何错误吗?

TaskCompletionSource

更新2:另一个很棒的解决方案

事实证明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; } 的工作方式与BlockingCollection类似,但也有接受超时和取消令牌的方法。这个解决方案的一个好处是它可以更新,以便轻松地生成ConcurrentQueue

WaitForNEvents

4 个答案:

答案 0 :(得分:5)

您可以使用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)一起使用,则必须创建单独的重载。我不认为有一种简单的方法可以使它适用于任何签名,特别是因为参数的数量并不总是相同的。

答案 1 :(得分:2)

您可以使用Rx将事件转换为可观察事件,然后转换为任务,最后使用令牌/超时等待该任务。

这比任何现有解决方案都有一个优势,就是它会在事件的线程上调用unsubscribe确保您的处理程序无法被调用两次。 (在您的第一个解决方案中,您可以通过tcs.TrySetResult而不是tcs.SetResult来解决此问题,但是总是很高兴摆脱&#34; TryDoSomething&#34;并确保DoSomething始终有效)。

另一个优点是代码的简单性。它基本上是一行。所以你甚至不需要独立的功能。您可以对其进行内联,以便更准确地了解代码的作用,并且您可以在不需要大量可选参数(例如可选initializer)的情况下对主题进行修改,或者允许等待N个事件,或者在他们没有必要的情况下进行超时/取消)。并且,当它完成时,您的bool返回值范围内的实际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; }
}

答案 2 :(得分:0)

非常感谢! 帮助别人了解... (也许显示带有点击操作处理程序代码的serialdevice代码)

您还可以添加一个通用类型约束,例如添加

 where TEvent : EventArgs

在我的情况下,我还需要结果在“服务员”中出结果
所以我改变了签名
 (在通用对象上快速且丑陋...)

 public static bool WaitForSingleEventWithResult<TEvent, TObjRes>(
            this CancellationToken token,
            Func<TEvent, TObjRes> onEvent,
             ...

以这种方式调用

        var ct = new CancellationToken();
        object result;
        bool eventOccurred = ct.WaitForSingleEventWithResult<MyEventArgs, object>(
            onEvent: statusPacket => result = this.OnStatusPacketReceived(statusPacket),
            subscribe: sub => cp.StatusPacketReceived_Action += sub,
            unsubscribe: unsub => cp.StatusPacketReceived_Action -= unsub,
            msTimeout: 5 * 1000,
            initializer: /*() => serialDevice.RequestStatusPacket()*/null);

反正...非常感谢!

答案 3 :(得分:0)

为什么不只是使用 ManualResetEventSlim.Wait (int millisecondsTimeout, CancellationToken cancellationToken)