Even when you have complex logic for cancellation, you want to cancel the underlying tasks. If the underlying tasks are cancelled at the right time, you can use Task.WhenAll
in any case.
So breaking down your question, what you're asking is, 'How can I cancel tasks based on the state of other tasks?'. You need to keep a state of number of completed tasks and cancel your tasks based on that state.
If you need to do 'stuff' when tasks complete (like update the state of how many tasks completed), I find continuations to be helpful and quite a clean solution. Example of your usecase:
// n from your question
var n = 4;
// number of tasks currently completed
var tasksCompleted = 0;
// The list of tasks (note it's the continuations in this case)
// You can also keep the continuations and actual tasks in separate lists.
var tasks = new List<Task>();
// delay before cancellation after n tasks completed
var timeAfterNCompleted = TimeSpan.FromMinutes(x);
using var cts = new CancellationTokenSource();
for (int i = 0; i < 10; i++)
{
// Do your work with a passed cancellationtoken you control
var currentTask = DoWorkAsync(i, cts.Token);
// Continuation will update the state of completed tasks
currentTask = currentTask.ContinueWith((t) =>
{
if (t.IsCompletedSuccessfully)
{
var number = Interlocked.Increment(ref tasksCompleted);
if (number == n)
{
// If we passed n tasks completed successfully,
// We'll cancel after the grace period
// Note that this will actually cancel the underlying tasks
// Because we passed the token to the DoWorkAsync method
cts.CancelAfter(timeAfterNCompleted);
}
}
});
tasks.Add(currentTask);
}
await Task.WhenAll(tasks);
// All your tasks have either completed or cancelled here
// Note that in this specific example all tasks will appear
// to have run to completion. That's because we're looking at
// the continuations here. Store continuation and actual task
// in separate lists and you can retrieve the results.
// (Make sure you await the continuations though)