35
votes

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 methods returns true.

My question is:

What happens with all remaining tasks that have been launched and probably still running in the background? Is this the right approach to execute a 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?

Thanks

1
I recommend to use a common cancellation token. After 1st task has ended you should send cancellation to all other tasks.Leonid Malyshev
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
Do you think Task.WhenAll would be a good fit for what you want?heltonbiker
@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
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

1 Answers

24
votes

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.