1

Currently I have a code like this:

 bool task1Result = await RunTask1(data);
 if(!task1Result)
     return false;

 bool task2Result = await RunTask2(data);
 if(!task2Result)
     return false;

 bool task3Result = await RunTask3(data);
 if(!task3Result)
     return false;

 bool task4Result = await RunTask4(data);
 if(!task4Result)
     return false;

Added sample:

private async Task<bool> RunListOfTasks() {
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;

var tasks = new List<Task<bool>> { RunTask1(data, ct), RunTask2(data, ct), RunTask3(data, ct), RunTask4(data, ct) };
while (tasks.Any())
{
    var currentTask = await Task.WhenAny(tasks);
    if (!await currentTask)
    {
        ct.Cancel();
        return false;
    }
    tasks.Remove(currentTask);
}
return true;
}

Is it possible to run all of them in parallel and if one of them fails (like result is false), then stop processing the rest and return. Thanks

Gauravsa
  • 6,330
  • 2
  • 21
  • 30
  • 1
    Each run task method would need to be changed to be cancellable e.g. by accepting a `CancellationToken`. Then you could use `Task.WhenAll` to parallelise. – Johnathan Barclay Jun 01 '22 at 09:33
  • Tasks aren't threads. You don't cancel the tasks, you cancel the actions/jobs that are executed by those tasks. Your methods need to accept a `CancellationToken` parameter and check it to see if cancellation was signalled – Panagiotis Kanavos Jun 01 '22 at 09:34
  • `if one of them fails (like result is false),` that's not what `fail` means. A `false` result means the task succeeded and returned `false`. Failure means that an exception was thrown. You can catch any failure easily if you use a try block around `await Task.WhenAll();` and signal the other methods to cancel through a `CancellationTokenSource`. It's a lot harder to do the same if you have to check every result. You won't be able to use `Task.WhenAll` in that case – Panagiotis Kanavos Jun 01 '22 at 09:38
  • What do those methods really do? Why do they return a `bool` instead of throwing? It matters. If they *have* to return a boolean, you should probably pass a `CancellationTokenSource` to each of them, to allow each task to signal cancellation before returning. Typically, cancellation flows from the outside in – Panagiotis Kanavos Jun 01 '22 at 09:40
  • yes, fail means that the task succeeded and the output of the task is false. – Gauravsa Jun 01 '22 at 09:50
  • i have edited and added some sample code. Is it possible to share some sample code – Gauravsa Jun 01 '22 at 09:51

2 Answers2

2

The Task.WhenAny-in-a-loop is generally considered an antipattern, because of its O(n²) complexity. The preferred approach is to wrap your tasks in another set of tasks, that will include the functionality of canceling the CancellationTokenSource when the result is false. Then await the wrapper tasks instead of the initial tasks, and propagate their result.

An easy way to wrap the tasks is the Select LINQ operator:

private async Task<bool> RunListOfTasks()
{
    using CancellationTokenSource cts = new();

    List<Task<bool>> tasks = new()
    {
        RunTask1(data, cts.Token),
        RunTask2(data, cts.Token),
        RunTask3(data, cts.Token),
        RunTask4(data, cts.Token),
    };

    Task<bool>[] enhancedTasks = tasks.Select(async task =>
    {
        try
        {
            bool result = await task.ConfigureAwait(false);
            if (!result) cts.Cancel();
            return result;
        }
        catch (OperationCanceledException) when (cts.IsCancellationRequested)
        {
            return false;
        }
    }).ToArray();

    bool[] results = await Task.WhenAll(enhancedTasks).ConfigureAwait(false);
    return results.All(x => x);
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

Microsoft's Reactive Framework does this in a very nice way:

bool[] result =
    await 
        Observable
            .Merge(
                Observable.FromAsync(ct => RunTask1(data, ct)),
                Observable.FromAsync(ct => RunTask2(data, ct)),
                Observable.FromAsync(ct => RunTask3(data, ct)),
                Observable.FromAsync(ct => RunTask4(data, ct)))
            .TakeUntil(x => x == false)
            .ToArray();

It returns all of the results that come in right up to the point one of the tasks returns a false.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • This is a decent solution, with the caveat that the `await` might return before all started `RunTaskX` operations have completed. In other words it allows fire-and-forget to occur. [Here](https://stackoverflow.com/questions/70901276/how-to-implement-a-custom-selectmany-operator-that-waits-for-all-observable-subs "How to implement a custom SelectMany operator that waits for all observable subsequences to complete?") is a question related to this issue. – Theodor Zoulias Jun 08 '22 at 07:30
  • @TheodorZoulias - No, depending on how each task is implemented, the fact that the observable stops, then the tasks stop proactively. – Enigmativity Jun 08 '22 at 09:23
  • Do you mean that fire-and-forget is impossible with the answer above? I am just claiming is that fire-and-forget is possible, not that it will happen always. And depending on the scenario, fire-and-forget might be anything from OK to unacceptable. – Theodor Zoulias Jun 08 '22 at 09:33
  • It depends on how you write the task methods as to if they fire and forget or if they terminate. – Enigmativity Jun 08 '22 at 09:39
  • True. So if you have a scenario that makes fire-and-forget unacceptable, and you don't know with 100% certainty that all the asynchronous methods that you are calling are reacting instantaneously to a cancellation signal, then the above solution is not appropriate. – Theodor Zoulias Jun 08 '22 at 09:44
  • @TheodorZoulias - Sure. If you say so. Time and money is usually the thing. If you have enough then anything can be done. – Enigmativity Jun 08 '22 at 09:52
  • I think that it's more of a case of choosing the right tool for the job. If you choose the wrong tool, the mistake can be expensive. – Theodor Zoulias Jun 08 '22 at 09:55
  • @TheodorZoulias - Sure, are you saying Rx is the wrong tool or is it TPL? – Enigmativity Jun 08 '22 at 10:04
  • Unless I knew for sure that fire-and-forget is harmless for my scenario, I would stay away from Rx for solving this problem. TBH I would avoid the Rx in all scenarios because I have a personal aversion for fire-and-forget in general. And it's not that using Rx results to a significantly more readable code. A `Task.WhenAll`-based or `Parallel.ForEachAsync`-based solution is pretty compact anyway, not to mention that it doesn't require a dependency to a NuGet package, and it has a smoother learning curve. – Theodor Zoulias Jun 08 '22 at 10:17
  • 1
    @TheodorZoulias - Save Rx for the priesthood... – Enigmativity Jun 08 '22 at 10:30
  • A simple `.ContinueWith(t=>{ if (t.IsCompletedSuccesfully && !t.Result) cts.Cancel();})` for each task would be cheaper - no observables and scheduling, no state machine allocations. The operation doesn't block so it doesn't really matter where the continuation runs but `TaskContinuationOptions.RunContinuationsAsynchronously` can be used to ensure the continuation runs asynchronously. `OnlyOnRanToCompletion` could be used to ensure exceptions aren't swallowed – Panagiotis Kanavos Jun 09 '22 at 15:39