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

时间:2017-02-21 21:23:36

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

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

2 个答案:

答案 0 :(得分:8)

你不应该使用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)
{
  ...
}

答案 1 :(得分:0)

根据@ 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;
}

欢迎评论,试一试!