等待具有超时的任务

10

我正在尝试编写一个辅助方法,它允许我传入任意任务和超时时间。如果任务在超时之前完成,则调用成功委托,否则调用错误委托。该方法如下:

    public static async Task AwaitWithTimeout(Task task, int timeout, Action success, Action error)
    {
        if (await Task.WhenAny(task, Task.Delay(timeout)) == task)
        {
            if (success != null)
            {
                success();
            }
        }
        else
        {
            if (error != null)
            {
                error();
            }
        }
    }

现在这个似乎大部分时间都能起作用,但我也想写一些测试来确保。但这个测试让我惊讶的失败了,并调用了错误委托而不是成功委托:

        var taskToAwait = Task.Delay(1);
        var successCalled = false;

        await TaskHelper.AwaitWithTimeout(taskToAwait, 10, () => successCalled = true, null);

        Assert.IsTrue(successCalled);

然而,这个测试是绿色的:

        var taskToAwait = Task.Run(async () =>
        {
            await Task.Delay(1);
        });

        var successCalled = false;

        await TaskHelper.AwaitWithTimeout(taskToAwait, 10, () => successCalled = true, null);

        Assert.IsTrue(successCalled);

我该如何使这两个测试都通过?我的 Task.WhenAny 用法是否不正确?


1
"Delay(1)" 很短(尤其是在调试时)- 在单独的线程上启动它会使测试大多数情况下通过。尝试使用更长的延迟或更好的任务,允许在测试中手动同步完成。 - Alexei Levenkov
1个回答

17
计时器不准确,其默认精度大约为15毫秒。任何低于该值的时间间隔都将以15毫秒的间隔触发。参考相关答案。 假设你有1毫秒计时器和10毫秒计时器;两者大致相等,因此结果不一致。
你用Task.Run封装的代码并声称它在工作只是巧合。当我尝试了几次后,结果不一致。它有时会因为上述原因而失败。
最好增加超时时间或者直接传入已经完成的任务。
例如,下面的测试应该始终通过。记住,你的测试应该是一致的而不是脆弱的。
[Test]
public async Task AwaitWithTimeout_Calls_SuccessDelegate_On_Success()
{
    var taskToAwait = Task.FromResult(0);

    var successCalled = false;

    await TaskHelper.AwaitWithTimeout(taskToAwait, 10, () => successCalled = true, ()=>{ });

    Assert.IsTrue(successCalled);
}

对于永远不会结束的任务,请使用TaskCompletionSource,不要设置其结果。

[Test]
public async Task AwaitWithTimeout_Calls_ErrorDelegate_On_NeverEndingTask()
{
    var taskToAwait = new TaskCompletionSource<object>().Task;

    var errorCalled = false;

    await TaskHelper.AwaitWithTimeout(taskToAwait, 10, () => { }, ()=> errorCalled = true);

    Assert.IsTrue(errorCalled);
}

我建议你避免使用null。你可以将空的委托作为参数传递。这样你就不需要在代码库中到处添加null检查。

我会将这个帮助方法写成:

public static async Task AwaitWithTimeout(this Task task, int timeout, Action success, Action error)
{
    if (await Task.WhenAny(task, Task.Delay(timeout)) == task)
    {
        success();
    }
    else
    {
        error();
    }
}
请注意,上述方法是扩展方法,因此您可以使用任务实例调用它。
await taskToAwait.AwaitWithTimeout(10, () => { }, ()=> errorCalled = true);//No nulls, just empty delegate

谢谢。您有任何建议如何测试错误场景吗?也就是输入一个永远无法完成的任务。目前,我仍在使用Task.Delay()进行测试(只需将延迟设置得非常高,超时时间设置得有点低)。 - user1202032
1
@user1202032 当然。我更新了我的答案,以解决无休止的任务问题。 - Sriram Sakthivel
1
尽管核心概念(使用Task.WhenAny等待两个任务)很精确,但其周围的API感觉很糟糕。如果需要从中连接另一个任务,则成功的“Action”会很混乱 - 这样你最终会得到丑陋的捕获结果。我会在超时情况下简单地抛出TimeoutException,并将成功完成视为成功完成。在性能关键的情况下,Task<bool>返回类型也可以正常运行(其中true表示成功完成,而false表示超时)。 - Kirill Shlenskiy
还有处理超时的Task最终完成的问题(在这里讨论:http://blogs.msdn.com/b/pfxteam/archive/2012/10/05/how-do-i-cancel-non-cancelable-async-operations.aspx)。当然,不要忘记在帮助程序或库方法中使用`ConfigureAwait(false)` - 没有任何理由不这样做。 - Kirill Shlenskiy
@KirillShlenskiy 是的,完全同意我的想法。Task<bool>比委托更有意义。甚至更好的是TimeoutExceptiion。我把这个决定留给OP。 - Sriram Sakthivel

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