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
这个选项并不错,但是ManualResetEventSlim
的Dispose
处理比看起来要复杂。我在闭包中访问了已修改或已释放的内容,这让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);
}
}
问题
我对这两种解决方案都不是特别满意,而且我也不确定它们是否完全正确。这两种方案中有哪一种更好(习惯用语、效率等),或者是否有更简单的方法或内置函数可以满足我在这里需要做的事情?
更新:目前最佳答案
下面是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
相似,但还有接受超时和取消标记的方法。这个解决方案中的一个好处是它可以很容易地更新为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();
}
}
ManualResetEventSlim
相同的Dispose
麻烦中,或者你有什么解决方法吗? - lobsterismserialDevice.RequestStatusPacket()
发送命令给我的设备以响应状态数据包,并且我需要等待响应,在不同的线程上使用超时或取消来处理事件并在自己的线程上处理该事件。不确定为什么这不合理。 - lobsterism