gpt4 book ai didi

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

转载 作者:可可西里 更新时间:2023-11-01 03:02:01 25 4
gpt4 key购买 nike

所以我的要求是让我的函数等待第一个实例 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 适合我在闭包中访问修改/处置的东西,而且真的很难理解,所以我什至不确定它是否正确。也许我缺少一些可以清理它的东西,这是我的偏好,但我不会立即看到它。这是代码。

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% 正确的。这些解决方案中的任何一个比另一个更好(惯用性、效率等),还是有更简单的方法或内置函数来满足我在这里需要做的事情?

更新:迄今为止的最佳答案

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

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

结果是 BlockingCollection就像 ConcurrentQueue 一样工作但也有接受超时和取消 token 的方法。这个解决方案的一个好处是它可以更新为 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();
}
}

最佳答案

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

关于c# - 如何在 C# 中等待单个事件,超时和取消,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17635440/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com