Chris Davies
Chris Davies

Reputation: 39

Design pattern with multiple cancellable tasks and unhandled exception handling

I want a C# console app that

  1. Runs multiple tasks
  2. Cancellable by Control C
  3. Unhandled exceptions in one task, cleanly cancel the other tasks.

The below code deals with the cancellation correctly, but how to make unhandled exceptions in one task cleanly cancel the other tasks?

public static async Task<int> Main ()
{
    CancellationTokenSource cts = new();

    Console.CancelKeyPress += ( sender, e ) =>
    {
        e.Cancel = true;
        cts.Cancel ();
    };

    try
    {
        var taskA= Task.Run( async () =>
        {
            ...
        }, cts.Token);

        var taskB= Task.Run( async () =>
        {
            ...
        }, cts.Token);

        var taskC= Task.Run( async () =>
        {
            ...
        }, cts.Token);


        await Task.WhenAll ( [taskA,TaskB, TaskC] );
        return 0;
    }
    catch ( OperationCanceledException )
    {
        .....
        return 0;
    }
    catch ( Exception ex )
    {
        .......
        return 1;
    }
}

Upvotes: 1

Views: 67

Answers (1)

Guru Stron
Guru Stron

Reputation: 143098

Arguably this is one of the rare case when it seems OKish to use ContinueWith:

var startNew = Stopwatch.StartNew();
var cts = new CancellationTokenSource();
var task1 = Task.Run(async () =>
{
    await Task.Delay(500, cts.Token);
    throw new Exception("1");
});
var task2 = Task.Run(async () =>
{
    await Task.Delay(50_000, cts.Token);
});

var tasks = new[] { task1, task2 };
// add continuation which will cancel token on fault of any task 
foreach (var t in tasks)
{
    _ = t.ContinueWith(_ => cts.Cancel(), 
      cts.Token, 
      TaskContinuationOptions.OnlyOnFaulted, 
      TaskScheduler.Default);
}

try
{
    await Task.WhenAll(tasks); // wait for tasks
}
catch (Exception e)
{
    Console.WriteLine(e.Message); 
}

Console.WriteLine($"Elapsed: {startNew.ElapsedMilliseconds}"); //  Elapsed: 530

Though I would consider using Parallel.ForEach(Async) which supports fail fast out of the box AFAIK (though note that you might need to play with the level of parallelism). Code can look something like the following:

var cts = new CancellationTokenSource();

int[] delays = [500, 50_000];
try
{
    await Parallel.ForEachAsync(delays, cts.Token, async (delay, ct) =>
    {
        await Task.Delay(delay, ct);
        if (delay == delays.Min())
        {
            throw new Exception("1");
        }
    });
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Console.WriteLine($"Elapsed:{startNew.ElapsedMilliseconds}"); // Elapsed:532

Notes:

  1. Make sure that actions passed to Task.Run actually handle the token since cancellation is cooperative
  2. See also

Upvotes: 0

Related Questions