所以我的要求是让我的函数等待来自另一个类和另一个线程的event Action<T>
的第一个实例,并在我的线程上处理它,允许等待被超时或{{1 }}。
我想创建一个可以重用的泛型函数。我设法创建了一些(我认为)我需要的选项,但两者看起来都比我想象的要复杂得多。
为了清楚起见,这个函数的示例使用看起来像这样,CancellationToken
在一个单独的线程上吐出事件:
serialDevice
这个选项并不错,但var eventOccurred = Helper.WaitForSingleEvent<StatusPacket>(
cancellationToken,
statusPacket => OnStatusPacketReceived(statusPacket),
a => serialDevice.StatusPacketReceived += a,
a => serialDevice.StatusPacketReceived -= a,
5000,
() => serialDevice.RequestStatusPacket());
处理Dispose
比看起来应该更糟糕。它让ReSharper适合我在闭包内访问修改/处理的东西,而且真的很难遵循,所以我甚至不确定它是否正确。也许有一些我遗漏的东西可以清理它,这将是我的偏好,但我不会随便看到它。这是代码。
ManualResetEventSlim
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
事实证明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
答案 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)
?