什么是防止任务失控的最佳方法?最好的结束任务方式是什么?

3
我创建了下面的函数,它将等待所有任务完成或在取消或超时时引发异常。
public static async Task WhenAll(
    IEnumerable<Task> tasks, 
    CancellationToken cancellationToken,
    int millisecondsTimeOut)
{
    Task timeoutTask = Task.Delay(millisecondsTimeOut, cancellationToken);
    Task completedTask = await Task.WhenAny(
        Task.WhenAll(tasks), 
        timeoutTask
    );
    if (completedTask == timeoutTask)
    {
        throw new TimeoutException();
    }
}

如果所有的任务在长时间超时(即millisecondsTimeOut= 60,000)之前都已经完成,那么timeoutTask会一直保持在原地直到60秒过去,甚至在函数返回后也是如此吗?如果是,最好的方法是什么来解决这个问题?

2
它会一直存在,但非常轻量级。只要你没有成百上千个这样的东西挂起来,如果让它运行到完成,你可能不会看到任何区别。然而,你可以在你的方法中创建一个CancellationTokenSource,将该CancellationToken传递给Task.Delay。在你的方法结束时,只需取消该令牌,就可以终止延迟。 - Jacob Roberts
如果这个被调用了数百次每秒,那么这可能会成为非常显著的开销。你正在冒着内存使用爆炸的风险。这是一种定时内存泄漏。在测试期间无法发现。它使应用程序变得不可靠。 - usr
函数的目的是什么?作为信号机制,还是在工作线程执行时暂停主(或监视)线程?如果您只想杀死所有工作线程,则有CancellationTokenSource,所以只需使用它即可... - code4life
1个回答

2
是的,timeoutTask会一直存在,直到超时结束(或CancellationToken被取消)。
您可以通过传入来自新的CancellationTokenSource创建的不同CancellationToken来解决此问题,并在最后取消。您还应该等待完成的任务,否则您实际上并没有观察异常(或取消):
public static async Task WhenAll(
    IEnumerable<Task> tasks,
    CancellationToken cancellationToken,
    int millisecondsTimeOut)
{
    var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    var timeoutTask = Task.Delay(millisecondsTimeOut, cancellationTokenSource.Token);
    var completedTask = await Task.WhenAny(Task.WhenAll(tasks), timeoutTask);
    if (completedTask == timeoutTask)
    {
        throw new TimeoutException();
    }

    cancellationTokenSource.Cancel();
    await completedTask;
}

然而,如果你不需要区分TimeoutExceptionTaskCancelledException,我认为有一种更简单的方法可以实现你想要的功能。你只需要添加一个在CancellationToken被取消或超时结束时被取消的延续:

public static Task WhenAll(
    IEnumerable<Task> tasks,
    CancellationToken cancellationToken,
    int millisecondsTimeOut)
{
    var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    cancellationTokenSource.CancelAfter(millisecondsTimeOut);

    return Task.WhenAll(tasks).ContinueWith(
        _ => _.GetAwaiter().GetResult(), 
        cancellationTokenSource.Token,
        TaskContinuationOptions.ExecuteSynchronously, 
        TaskScheduler.Default);
}

您能否详细说明为什么在结尾添加了 await completedTask?我认为 Task.WhenAll 会等待所有任务完成、取消或出现异常。如果函数由于超时而返回,则由调用者负责取消/结束任务或执行其他工作。 - Tony
1
因为 Task.WhenAny 的签名是 Task<Task>,所以第二个 await 是为了观察内部任务的结果,以便如果它抛出异常,其异常可以向上传播到调用堆栈。 - Scott Chamberlain
1
@Tony Task.WhenAll 如果其中一个任务抛出异常,它会抛出异常... 但是 Task.WhenAny 不会。如果你想让调用者知道有异常发生,你需要等待完成的任务并重新抛出异常。 - i3arnon

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