50

I have the following code:

List<Task<bool>> tasks = tasksQuery.ToList();
while (tasks.Any())
{
    Task<bool> completedTask = await Task.WhenAny(tasks);
    if (await completedTask)
        return true;

    tasks.Remove(completedTask);
}

It launches tasks in parallel. When first completed task returns true, the method returns true.

My questions are:

  1. What happens with all remaining tasks that have been launched and probably still running in the background?
  2. Is this the right approach to execute code that is async, parallel and should return after the first condition occurs, or it is better to launch them one by one and await singularly?
Bondolin
  • 2,793
  • 7
  • 34
  • 62
Aleksander Bethke
  • 1,359
  • 2
  • 11
  • 24
  • 6
    I recommend to use a common cancellation token. After 1st task has ended you should send cancellation to all other tasks. – Leonid Malyshev Jan 02 '17 at 12:00
  • 6
    This question is a little broad. "What happens?" well they keep executing until they finish..."Is this the right approach?" How should we know? We don't know what those tasks are doing. If you launch them one by one and wait for each to check, it's no longer parallel or async and the question is contradicting itself... you may want to pass a cancellation token if you're worried that remaining taks are wasting resources or causing other harm.... – René Vogt Jan 02 '17 at 12:02
  • 3
    Do you think `Task.WhenAll` would be a good fit for what you want? – heltonbiker Jan 02 '17 at 12:05
  • @RenéVogt The idea was to await each one so still async but not parallel. The token approach seems the best one. Thanks. – Aleksander Bethke Jan 02 '17 at 13:04
  • @heltonbiker The point was to execute "just enough" tasks and not every one of them to not waste resources. – Aleksander Bethke Jan 02 '17 at 13:07
  • 3
    Well, one thing for sure, when you create them all up front and run them in parallel, then it's certainly not "creating 'just enough' not to waste resources". In this scheme, the code is actually free to waste ALL the jobs/resources it can. Actually, that still holds evenif you implement cancellationtoken - with that, the code will still be free to use all jobs/resources it can, just to cancel/release/rollback/free/etc when first one completes. That's almost a definition of resource-vs-speed tradeoff - wasting jobs to get the result a bit faster. – quetzalcoatl Jan 02 '17 at 13:43
  • Related: [Task.WhenAny with cancellation of the non completed tasks and timeout](https://stackoverflow.com/questions/56207326/task-whenany-with-cancellation-of-the-non-completed-tasks-and-timeout) – Theodor Zoulias Sep 20 '21 at 02:55

1 Answers1

33

Incidentally, I am just reading Concurrency in C# CookBook, by Stephen Cleary, and I can refer to some parts of the book here, I guess.

From Recipe 2.5 - Discussion, we have

When the first task completes, consider whether to cancel the remaining tasks. If the other tasks are not canceled but are also never awaited, then they are abandoned. Abandoned tasks will run to completion, and their results will be ignored. Any exceptions from those abandoned tasks will also be ignored.

Another antipattern for Task.WhenAny is handling tasks as they complete. At first it seems like a reasonable approach to keep a list of tasks and remove each task from the list as it completes. The problem with this approach is that it executes in O(N^2) time, when an O(N) algorithm exists.

Besides that, I think WhenAny is surely the right approach. Just consider the following Leonid approach of passing the same CancellationToken for all tasks and cancel them after the first one returns. And even that, only if the cost of these operations is actually taxing the system.

MarredCheese
  • 17,541
  • 8
  • 92
  • 91
heltonbiker
  • 26,657
  • 28
  • 137
  • 252
  • 1
    Great, thanks. Shame on me I've missed that recipe in Stephen book. In my case I will just leave them out because those tasks do not perform any heavy lifting (1 HTTP call and some deserialization work each). In case Tasks are doing some resource taxing work I will use Cancellation tokens and suggested by some users in this thread. Thanks again. – Aleksander Bethke Jan 02 '17 at 13:36
  • 3
    Nice. Notice that you most probably would use a _single_ CancellationToken for all the Tasks. – heltonbiker Jan 02 '17 at 13:56
  • 18
    What is the O(N) approach? – nawfal May 29 '18 at 07:11
  • @nawfal probably to use a dictionary/set instead. – freakish Aug 07 '18 at 15:59
  • 1
    I went to GitHub to read the Task class code and i don't see a reason why it becomes O(N^2) just because you use the WhenAny method – Rafa Dec 17 '19 at 12:50
  • 3
    Here's more discussion of the O(n^2) stuff: https://devblogs.microsoft.com/pfxteam/processing-tasks-as-they-complete/ – Will Dean Apr 05 '20 at 22:42
  • 2
    It's not O(N^2) _time_ but it's O(N^2) complexity/overhead because each call to .WhenAny() schedules a completion (or, at least, checks whether a completion must be scheduled) of the remaining tasks. – user1969177 May 25 '21 at 00:00
  • @freakish It's still O(N^2) because each consecutive `Task.WhenAny()` has to iterate through all remaining items. – relatively_random Jan 27 '23 at 15:54
  • @nawfal Cleary himself implemented it here as `OrderByCompletion()`: https://github.com/StephenCleary/AsyncEx/blob/master/src/Nito.AsyncEx.Tasks/TaskExtensions.cs It's pretty cool. In a nutshell, the method constructs an array of task completion sources and then attaches continuations onto the original tasks. As tasks complete, they grab the next unused TCS and complete it. Tasks that complete first grab the first TCSs. – relatively_random Jan 30 '23 at 12:43