如何在不引发异常的情况下取消任务延迟或使用异常来控制流程?

20

我不确定如何对代码中的事件做出两种可能的反应。我主要担心哪一种需要更少的资源。

我有一个方法,其中注册了一个观察者到一个eventproducer。如果eventproducer返回了什么,该方法将退出,并且方法的调用者将再次启动该方法(可以将其视为一种长轮询)。

eventproducer有时会每秒触发很多事件,有时则会休息几分钟。

第一种方法是等待500ms的延迟,然后检查是否有返回内容,如果没有,则在500ms的延迟之后再次检查(直到5分钟的超时时间)。

eventProducer.RegisterListener((events)=>{evList.Add(events)});
while(evList.Count=0 && !TimeOut){
    await Task.Delay(500);}
eventProducer.UnRegister(...);
// return evList, method gets recalled immediately
第二种方法是使用 CancellationToken。如果 eventproducer 产生了一些东西,CancellationTokenSource 将取消源。在该方法中,我等待 Task.Delay(5min, cancellationToken)
eventProducer.RegisterListener((events)=>{evList.Add(events);
                                          cancelSource.Cancel();}
try
{
     await Task.Delay(5min, cancellationToken)
}
catch(TaskCanceledException){//nothing to do};
eventProducer.UnRegister(...);
// return evList, method gets recalled immediately
第二种方法的优点是,如果生产者生成了东西,该方法会立即返回,我们不需要在循环中等待和唤醒。但是,使用第二种方法时,每当生产者生成内容时,都会抛出一个TaskCanceledException异常。我担心这可能会比每500毫秒唤醒和等待的负载更大,特别是在事件生产者生成大量事件时。我是否高估了抛出和捕获异常的成本?是否有一种方法可以使用CancellationToken取消Task.Delay而不抛出TaskCanceledException异常?例如,类似于task.setComplete的东西?

这只是“更长时间”的占位符。 - xartal
1个回答

29

如果您想要类似于取消操作但不包含异常的即时通知,可以使用TaskCompletionSource

TaskCompletionSource是创建承诺任务的方式。从Task属性中获取未完成的任务,并使用SetResult将其完成(或取消)。您可以使用它来传递结果本身:

var tcs = new TaskCompletionSource<Events>();
eventProducer.RegisterListener(events => tcs.SetResult(events));

var result = await tcs.Task;
eventProducer.UnRegister(...);

这种解决方案没有任何异常并且不使用不必要的轮询。


回答您具体的问题:

我是否高估了抛出和捕获异常的成本?

可能是。您需要测试并证明它确实是一个问题。

有没有一种方法可以使用CancellationToken取消Task.Delay,但不会抛出TaskCanceledException?

有。添加一个空的延续(continuation):

var delayTask = Task.Delay(1000, cancellationToken);
var continuationTask = delayTask.ContinueWith(task => { });
await continuationTask;

谢谢,TaskCompletionSource正是我所寻找的! - xartal
4
使用“await Task.Delay(1000, cancellationToken).ContinueWith(task => { });”时,为什么不再抛出任务取消异常?在我添加try-catch之前,框架是否仍然需要在内部处理catch并处理异常,或者根本不会首先引发异常? - Snellface
2
@Snellface,Task.Delay 任务仍然被取消,如果您等待它,它仍然会抛出异常。但是您没有等待它...您正在等待一个什么都不做的继续操作。由于它什么也不做,并且没有 CancellationToken,因此它无法抛出任何异常。 - i3arnon
@i3arnon 所以使用 TaskCompletionSource 会更有效率,即便只是在微小的程度上?我第一次听说它是在这个问答环节中,所以对此不太熟悉。我的看法是取消一个延时操作比提前完成更符合语义学,但是如果取消的成本比完成的成本高得多,在这种情况下语义差异就可以忽略不计了。我也可能错了,认为任务必须在内部处理异常,但那是我理解的方式,只是当我不等待时就不需要捕获它,对吧? - Snellface
2
@Snellface 实际上在这种情况下没有异常..但对于除了 Task.Delay 之外的其他东西可能会有异常。等待已取消的任务会抛出异常,但您不需要异常来创建已取消的任务。想象一下 Task.Delay 方法返回一个 TaskCompletionSource,而不是调用 tcs.SetResult,它使用 tcs.SetCanceled。这样,delayTask 就被取消了,等待它将抛出异常,而任务本身不会捕获任何异常。 - i3arnon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接