Reputation: 4975
I have multiple tasks that accept a cancellation token and call ThrowIfCancellationRequested
accordingly. These tasks will run concurrently using Task.WhenAll
. I want all tasks to be cancelled when any tasks throw an exception. I achieved this using Select
and ContinueWith
:
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?
}
I'm not sure if this is the best way to do this, it seems to have some issues. It appears the exception will be caught internally, code after WhenAll
is always reached. I don't want the code after WhenAll
to be reached when an exception has occurred, I'd rather have the exception to be thrown so I can catch it manually on another level of the call stack. What's the best way to achieve this? If possible I'd like the call stack to remain intact. If multiple exceptions occur it would be best if only the first exception is rethrown, no AggregateException
.
On a related note, I tried passing the cancellation token to ContinueWith
like so: task.ContinueWith(lambda, cts.Token)
. However, when an exception in any task occurs, this will eventually throw a TaskCanceledException
instead of the exception I'm interested in. I figured I should pass the cancellation token to ContinueWith
because this would cancel ContinueWith
itself, which I don't think is what I want.
Upvotes: 13
Views: 12875
Reputation: 43475
What you are doing with the Select
and ContinueWith
is basically correct. The thing that is missing is the propagation of the status of the antecedent task to the continuation. The best way to do this is by return
ing the antecedent task, and unwrapping the resulting Task<Task>
using the Unwrap
method. This way every detail of the antecedent task is propagated, such as the canceling CancellationToken
, or multiple exceptions if present. Also this way there is no added friction caused by catching and rethrowing exceptions, and also ugly nested AggregateException
s are avoided.
/// <summary>
/// Creates a task that will complete when all of the supplied tasks have completed.
/// If any task completes as faulted, the CancellationTokenSource is canceled.
/// </param>
public static Task WhenAll_OnFaultedCancel(Task[] tasks, CancellationTokenSource cts)
{
ArgumentNullException.ThrowIfNull(tasks);
ArgumentNullException.ThrowIfNull(cts);
if (Array.IndexOf(tasks, null) != -1) throw new ArgumentException(
$"The {nameof(tasks)} collection contained a null task.");
IEnumerable<Task> continuations = tasks.Select(t => t.ContinueWith(t =>
{
if (t.IsFaulted) cts.Cancel();
return t;
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default).Unwrap());
return Task.WhenAll(continuations);
}
Usage example:
await WhenAll_OnFaultedCancel(tasks, cts);
Upvotes: 0
Reputation: 456457
You shouldn't use ContinueWith
. The correct answer is to introduce another "higher-level" async
method instead of attaching a continuation to each task:
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)
{
// ...
}
Upvotes: 19
Reputation: 1503
Based on @Stephen's answer, usage: await someTask.CancelOnError(cts)
It requires two extension methods to handle both Task and 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;
}
Comments are welcome, give it a try!
Upvotes: 2