【问题标题】:How to properly cancel Task.WhenAll and throw the first exception?如何正确取消 Task.WhenAll 并抛出第一个异常?
【发布时间】:2017-07-11 17:19:19
【问题描述】:

我有多个任务接受取消令牌并相应地调用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 本身,这我不认为这是我想要的。

【问题讨论】:

  • @Servy 这个问题和“重复”的区别在于这是关于Task.WhenAll 并在任务之外使用try-catch。另一个问题是关于加入多个ContinueWith 并明确检查task.Exception。
  • 差异不相关。 WhenAll 只是要使用ContinueWIth 附加它自己的延续,并且在确定它是否应该出现故障时检查Exception 值,给您留下完全相同的问题。其中一些是在 WhenAll 的幕后发生的,这与解释或纠正问题并没有太大的不同。
  • @Servy 好的,现在我知道我正在处理链式延续,我可以看到其他线程如何回答我的问题。但是,如果我没有问这个问题,我不会知道WhenAll 隐含地附加了它自己的延续,另一个线程没有触及这个主题。所以我看不出我的问题是如何重复的。
  • 我期待什么?我不知道。我对TPL的理解为零。感谢async/await,直到现在我才不得不使用ContinueWith。只是因为您的最后评论,我现在明白,向错误任务添加延续会导致任务不会出现错误。
  • 那么您没有阅读副本,因为它会告诉您很多。从字面上看,答案就是这样打开的。

标签: c# .net exception async-await task-parallel-library


【解决方案1】:

您不应该使用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;
}

欢迎评论,试一试!

【讨论】:

    猜你喜欢
    • 2020-08-05
    • 2017-08-06
    • 1970-01-01
    • 1970-01-01
    • 2021-01-20
    • 2012-05-13
    • 2020-06-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多