在键盘事件中使用 CancellationToken 调用 Task.Delay 时出现 TaskCanceledException

Posted

技术标签:

【中文标题】在键盘事件中使用 CancellationToken 调用 Task.Delay 时出现 TaskCanceledException【英文标题】:TaskCanceledException when calling Task.Delay with a CancellationToken in an keyboard event 【发布时间】:2013-12-28 20:05:14 【问题描述】:

我正在尝试延迟处理从 WinRT 中的键盘事件调用的方法(示例中为 SubmitQuery()),直到在一段时间内(本例中为 500 毫秒)没有进一步的事件。

我只希望 SubmitQuery() 在我认为用户已经完成输入时运行。

使用下面的代码,我在 Task.Delay(500,cancellationToken.Token); 时不断收到 System.Threading.Tasks.TaskCanceledException叫做。请问我在这里做错了什么?

CancellationTokenSource cancellationToken = new CancellationTokenSource();

private async void SearchBox_QueryChanged(SearchBox sender, SearchBoxQueryChangedEventArgs args)


        cancellationToken.Cancel();
        cancellationToken = new CancellationTokenSource();

    await Task.Delay(500, cancellationToken.Token);

    if (!cancellationToken.IsCancellationRequested)
    
        await ViewModel.SubmitQuery();
    

【问题讨论】:

【参考方案1】:

如果您在ContinueWith() 中添加一个空操作,则不会引发异常。 异常会被捕获并传递给ContinueWith() 中的tsk.Exception 属性。但它使您免于编写使您的代码变得丑陋的 try/catch。

await Task.Delay(500, cancellationToken.Token).ContinueWith(tsk =>  );

更新:

不用编写代码来处理异常,布尔值会更简洁。 只有在预计会延迟取消时才首选此选项!。一种方法是创建一个辅助类(虽然我不太喜欢辅助类)

namespace System.Threading.Tasks

    public static class TaskDelay
    
        public static Task<bool> Wait(TimeSpan timeout, CancellationToken token) =>
            Task.Delay(timeout, token).ContinueWith(tsk => tsk.Exception == default);

        public static Task<bool> Wait(int timeoutMs, CancellationToken token) =>
            Task.Delay(timeoutMs, token).ContinueWith(tsk => tsk.Exception == default);
    


例如:

var source = new CancellationTokenSource();

if(!await TaskDelay.Wait(2000, source.Token))

    // The Delay task was canceled.

(别忘了处理源)

【讨论】:

太棒了,谢谢。节省了一些非常难看的 try/catch 代码 :) 谢谢!即使它被捕获,调试器也很烦人。我认为在这些.Delay 情况下,我们期望令牌被取消,因此甚至不应该引发/捕获异常,这完美地解决了这个问题。 @Alexsandro 那是因为它被捕获并且异常在Task&lt;TResult&gt; tsk 参数中传递。 .ContinueWith(tsk =&gt; tsk.Exception); 这样继续的任务就可以处理异常了。在这种情况下,我们不...... 我认为应该注意的是 ContinueWith 方法确实捕获并吸收了异常,但是 task.delay 之后的任何代码仍然会执行。因此,如果您在异步 Task 方法中调用它,这可能不是一个好主意,因为取消令牌的目的是取消当前任务。在这种情况下,您必须手动检查 IsCancellationRequested 的取消令牌,或者将 try/catch 留在那里.. 请注意,ContinueWith 方法被 Stephen Cleary 描述为 dangerous,除非明确指定了 TaskScheduler 参数。【参考方案2】:

这是意料之中的。当您取消旧的Delay 时,会引发异常;这就是取消的工作原理。您可以在Delay 周围放置一个简单的try/catch 来捕获预期的异常。

请注意,如果您想做这样的基于时间的逻辑,Rx 比async 更自然。

【讨论】:

能否请您提供一些资源的链接,以了解 Rx 如何更适合基于时间的逻辑 @Anupam:这是我自己的经验。【参考方案3】:

奇怪的是,取消异常似乎只在取消标记位于 Task.Delay 时引发。将令牌放在 ContinueWith 上,不会抛出取消异常:

Task.Delay(500).ContinueWith(tsk => 
   //code to run after the delay goes here
, cancellationToken.Token);

如果你真的想捕捉任何取消异常,你可以只链接另一个 .ContinueWith() - 它会被传递到那里。

【讨论】:

【参考方案4】:

另一种抑制等待任务异常的简单方法是将任务作为单个参数传递给Task.WhenAny

创建一个在任何提供的任务完成后完成的任务。

await Task.WhenAny(Task.Delay(500, token)); // Ignores the exception

这种方法的一个问题是它不能清楚地传达其意图,因此建议添加评论。另一个是它导致分配(因为Task.WhenAny的签名中的params)。

【讨论】:

【参考方案5】:

我认为值得添加评论,说明它为什么会这样工作。

关于Task.Delay 方法的TaskCancelledException,文档实际上是错误的或写得不清楚。 Delay 方法本身从不抛出该异常。它将任务转移到取消状态,而引发异常的确切原因是await。这里使用Task.Delay 方法并不重要。它与任何其他已取消任务的工作方式相同,这就是取消预期的工作方式。这实际上解释了为什么添加延续会神秘地隐藏异常。因为是await引起的。

【讨论】:

以上是关于在键盘事件中使用 CancellationToken 调用 Task.Delay 时出现 TaskCanceledException的主要内容,如果未能解决你的问题,请参考以下文章

如何使用JavaScript捕获iOS上的隐藏键盘事件

JS求助,键盘事件和单击事件,限制只执行一次

做UI最全的鼠标键盘事件!

如何在 Web UI 中使用 Javascript 键盘事件

总结Selenium WebDriver中一些鼠标和键盘事件的使用

Java Swing中键盘的处理