如何正确取消Task.WhenAll并抛出第一个异常?

11

我有多个任务,接受取消令牌并相应地调用 ThrowIfCancellationRequested。这些任务将使用 Task.WhenAll 同时运行。当任何任务抛出异常时,我希望所有任务都被取消。我使用 SelectContinueWith 实现了这一点:

var cts = new CancellationTokenSource();

try
{
    var tasks = new Task[] { DoSomethingAsync(cts.Token), ... } // multiple tasks here
        .Select(task => task.ContinueWith(task =>
        {
            if (task.IsFaulted)
            {
                cts.Cancel();
            }
        }));

    await Task.WhenAll(tasks).ConfigureAwait(false);
}
catch (SpecificException)
{
    // Why is this block never reached?
}

我不确定这是最好的方法,它似乎存在一些问题。异常似乎会在内部被捕获,WhenAll后面的代码总是会执行。当发生异常时,我不想让WhenAll后面的代码执行,而是希望将异常抛出以便我可以在调用堆栈的另一个级别上手动捕获它。如何实现最佳方法?如果可能的话,我希望调用堆栈保持完整。如果发生多个异常,则最好只重新抛出第一个异常,不使用AggregateException


另外,我尝试将取消标记传递给ContinueWith,例如:task.ContinueWith(lambda, cts.Token)。但是,当任何任务中发生异常时,这将最终引发一个TaskCanceledException而不是我感兴趣的异常。我认为应该将取消标记传递给ContinueWith,因为这将取消ContinueWith本身,而我认为这不是我想要的。


1
差异并不重要。WhenAll 只是使用 ContinueWith 添加自己的继续,当它确定是否应该故障它自身时,它将检查 Exception 值,留下了完全相同的问题。其中一些发生在 WhenAll 的后台并没有实质性的区别,解释或纠正问题都是一样的。 - Servy
@Servy,我不明白你的解释或其他线程如何回答我的问题,即如何使异常像通常一样被抛出,允许我在调用堆栈的另一个级别上处理异常。 - Rudey
5
好的,现在我明白我正在处理链接连续操作,我可以看到另一个线程回答了我的问题。然而,如果我没有问这个问题,我就不会知道WhenAll隐含地附加了它自己的连续操作,另一个线程并未涉及此主题。因此,我不认为我的问题是重复的。 - Rudey
3
我期待什么?我不知道。我对TPL一无所知。多亏了async/await,直到现在我从未使用过ContinueWith。只有因为你最后的评论,我才明白将一个继续添加到一个失败的任务会导致一个不会失败的任务。 - Rudey
1
那么你没有读重复的内容,因为它会告诉你同样的信息。这实际上是答案的开头。 - Servy
显示剩余5条评论
2个回答

16
你不应该使用ContinueWith。正确的做法是引入另一个“更高级”的async方法,而不是将继续操作附加到每个任务上。
private async Task DoSomethingWithCancel(CancellationTokenSource cts)
{
    try
    {
        await DoSomethingAsync(cts.Token).ConfigureAwait(false);
    }
    catch
    {
        cts.Cancel();
        throw;
    }
}

var cts = new CancellationTokenSource();
try
{
    var tasks = new Task[] { DoSomethingWithCancel(cts), ... };
    await Task.WhenAll(tasks).ConfigureAwait(false);
}
catch (SpecificException)
{
    // ...
}

2

基于@Stephen的回答,用法:await someTask.CancelOnError(cts)

这需要两个扩展方法来处理Task和Task`T:

public static async Task CancelOnError(this Task task, CancellationTokenSource cts)
{
    try
    {
        await task.ConfigureAwait(false);
    }
    catch
    {
        cts.Cancel();

        throw;
    }
}

public static async Task<TTaskResult> CancelOnError<TTaskResult>(this Task<TTaskResult> task, CancellationTokenSource cts)
{
    await ((Task)task).CancelOnError(cts);

    return task.Result;
}

欢迎评论,试一试吧!


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