In this post, we’re going to see when we should use Task.WhenAny
and Task.WhenAll
. I’m also going to explain the differences between Task.WaitAny
and Task.WaitAll
. Then I move on to show some example of good and bad usage of these constructs. We’re also going to see some situations such as when we need immediate processing and solution for it.
When Should You Use Task.WhenAny
We use Task.WhenAny
when we have a collection of tasks, but we only interested in the first finished task. It can happen for example when we have couple of async API that all of them do the same thing. But we want to receive the result from the one that return the result first.
Another scenario is when we need a back up mechanism. So if one of those tasks failed the other one completes and gives us the result. For that reason when we receive a completed task to await, if any exception happens, it is not propagated to the returned task. Remember that when we call the method on line 4 by calling GetHtmlResponseAsync
, the method is on its way to get the result. So WhenAny
in this case return the first task that is finished. Finally by awaiting the returned task we get the actual results.
Another important thing to note is that when the first task is finished and returned, others are still running. What happens here if we don’t cancel them is that they run to completion and get abandoned. So in these situations when we don’t need other tasks we can cancel them.
What we did here is we take a cancellation token as an argument and pass them to GetAsync
. So now as you can see in UseFirstRespondingUrlWithCancellationAsync
method, we can cancel the rest of the tasks when we get the needed result.
When Should You Use Task.WhenAll
We can use Task.WhenAll
to wait for a set of tasks to complete. We can also wait for each task in a loop. But that’ll be inefficient since we dispatch the tasks one at the time.
As you can see in the method above, we call GetStringAsync
method one at the time. What we want is to dispatch the tasks that get the strings simultaneously. In these situations we can use Task.WhenAll
. So the above code can be refactored as this.
In the code above we aggregated our tasks into a list and asynchronously waited for all of them. Another important thing is how Task.WhenAll
deal with exceptions. When one or more exceptions are thrown, the exceptions are going to be place on the task as a AggregateException
. If we use try catch in this situation, we only get the first exception from the collection of exceptions. If we need all of them, we can’t await the task and get the result both on the same line.
In the above code excerpt, we assign the task to a variable. We then await it and then use the previous task variable to get all exceptions.
Processing Tasks As They Complete
Another situation that might come up once in a while is when we need to do some processing on a task as soon as it complete. Because right now the way we’re using Task.WhenAll
, we get the results from all tasks at once. We don’t have the opportunity to make any changes to the result before firing off the next. The best solution is to create an intermediary method that do the await and process for us and await that method instead.
As you can see in the code above, the AwaitAndProcessAsync
method is the method that does the await and extra processing for us. We first create the tasks, then pipe them though AwaitAndProcessAsync
method and get a list of Task. We then use the Task.WhenAll
to wait for all of those task and processing them as soon as they return a value. There is an article by Stephen Toub that goes into much more detail.
Task.WhenAny vs Task.WaitAny
The main difference between these two is that Task.WaitAny
is a blocking operation. It is the same as using task.Wait()
or task.Result
. The difference is that it takes multiple tasks and return the result from the one that completed first. Task.WaitAny
can be used in some situations, but they are rare. Maybe when we want to block the operation for example in console app. But even that’s not acceptable these days. Because since C# 7.1 we can use async Task Main(string[] args)
.
Task.WhenAll vs Task.WaitAll
Like Task.WaitAny
, Task.WaitAll
also is a blocking operation. It behaves the same as task.Wait()
, except it takes a collection of tasks and wait for all of them to finish. Like its counter part, Task.WaitAll
is use rarely if at all. In most situations the non blocking Task.WhenAll
is what we should be using.
Summary
In this post I explained when and how to use Task.WhenAny
and Task.WhenAll
. We also saw their differences with Task.WaitAny
and Task.WaitAll
method and some best practices regarding using them. I also briefly gone though situations and solutions when we use Task.WhenAll and we need to do some processing immediately after a task returns value.
Thank you for this very good article and examples. But I wonder, how do you handle awaiting two tasks with different return values, such as Task and Task?