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

问题描述 投票:0回答:6

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

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

使用下面的代码,当 Task.Delay(500,cancelToken.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();
    }
}
c# asynchronous windows-runtime cancellationtokensource cancellation-token
6个回答
68
投票

如果添加带有空操作的

ContinueWith()
,则 不会引发异常。 异常将被捕获并传递到
tsk.Exception
中的
ContinueWith()
属性。但它可以让您免于编写丑化代码的 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.
}

(别忘了处理源)


26
投票

这是可以预料的。当你取消旧的

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

请注意,如果您想执行这样的基于时间的逻辑,Rx 比

async
更自然。


6
投票

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

该文档实际上是错误的,或者对于

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


6
投票

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

Task.WhenAny
:

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

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

这种方法的一个问题是它没有清楚地传达其意图,因此建议添加注释。另一个是它会导致分配(因为

params
签名中的
Task.WhenAny
)。


注意:

Task
发生故障且未直接或间接观察到其
Exception
时,会在未来某个时刻(非确定性)为此任务触发
TaskScheduler.UnobservedTaskException
事件。上面显示的技巧,使用
Task.WhenAny
方法,not 观察任务的
Exception
,因此事件 will 被触发。在
Task.Delay(500, token)
的特定情况下,任务以取消状态完成,而不是出现故障,因此无论如何它都不会触发事件。


1
投票

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

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

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


-1
投票

如果您不关心等待的任务可能引发的任何异常,您可以创建如下扩展方法:

public static class TaskExtensions
{
    public static async Task<bool> TryAsync(this Task task)
    {
        try
        {
            await task;
            return true;
        }
        catch
        {
            return false;
        }
    }
}

然后像这样使用它:

var wasCompleted = await Task.Delay(500, cancellationToken).TryAsync();
if (wasCompleted)
{
    // do something
}

额外

作为补充,您可以为

Task<T>
添加类似的扩展方法到
TaskExtensions
类。

public static async Task<(bool wasCompleted, T result)> TryAsync<T>(this Task<T> task)
{
    try
    {
        var result = await task;
        return (true, result);
    }
    catch
    {
        return (false, default(T));
    }
}

用途:

var (wasCompleted, result) = await GetSomethingAsync().TryAsync();
if (wasCompleted)
{
    var x = result.X;
}
© www.soinside.com 2019 - 2024. All rights reserved.