Why should I prefer single 'await Task.WhenAll' over multiple awaits?
Solution 1
Yes, use WhenAll
because it propagates all errors at once. With the multiple awaits, you lose errors if one of the earlier awaits throws.
Another important difference is that WhenAll
will wait for all tasks to complete even in the presence of failures (faulted or canceled tasks). Awaiting manually in sequence would cause unexpected concurrency because the part of your program that wants to wait will actually continue early.
I think it also makes reading the code easier because the semantics that you want are directly documented in code.
Solution 2
My understanding is that the main reason to prefer Task.WhenAll
to multiple await
s is performance / task "churning": the DoWork1
method does something like this:
- start with a given context
- save the context
- wait for t1
- restore the original context
- save the context
- wait for t2
- restore the original context
- save the context
- wait for t3
- restore the original context
By contrast, DoWork2
does this:
- start with a given context
- save the context
- wait for all of t1, t2 and t3
- restore the original context
Whether this is a big enough deal for your particular case is, of course, "context-dependent" (pardon the pun).
Solution 3
An asynchronous method is implemented as a state-machine. It is possible to write methods so that they are not compiled into state-machines, this is often referred to as a fast-track async method. These can be implemented like so:
public Task DoSomethingAsync()
{
return DoSomethingElseAsync();
}
When using Task.WhenAll
it is possible to maintain this fast-track code while still ensuring the caller is able to wait for all tasks to be completed, e.g.:
public Task DoSomethingAsync()
{
var t1 = DoTaskAsync("t2.1", 3000);
var t2 = DoTaskAsync("t2.2", 2000);
var t3 = DoTaskAsync("t2.3", 1000);
return Task.WhenAll(t1, t2, t3);
}
Solution 4
(Disclaimer: This answer is taken/inspired from Ian Griffiths' TPL Async course on Pluralsight)
Another reason to prefer WhenAll is Exception handling.
Suppose you had a try-catch block on your DoWork methods, and suppose they were calling different DoTask methods:
static async Task DoWork1() // modified with try-catch
{
try
{
var t1 = DoTask1Async("t1.1", 3000);
var t2 = DoTask2Async("t1.2", 2000);
var t3 = DoTask3Async("t1.3", 1000);
await t1; await t2; await t3;
Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
}
catch (Exception x)
{
// ...
}
}
In this case, if all 3 tasks throw exceptions, only the first one will be caught. Any later exception will be lost. I.e. if t2 and t3 throws exception, only t2 will be catched; etc. The subsequent tasks exceptions will go unobserved.
Where as in the WhenAll - if any or all of the tasks fault, the resulting task will contain all of the exceptions. The await keyword still always re-throws the first exception. So the other exceptions are still effectively unobserved. One way to overcome this is to add an empty continuation after the task WhenAll and put the await there. This way if the task fails, the result property will throw the full Aggregate Exception:
static async Task DoWork2() //modified to catch all exceptions
{
try
{
var t1 = DoTask1Async("t1.1", 3000);
var t2 = DoTask2Async("t1.2", 2000);
var t3 = DoTask3Async("t1.3", 1000);
var t = Task.WhenAll(t1, t2, t3);
await t.ContinueWith(x => { });
Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
}
catch (Exception x)
{
// ...
}
}
Solution 5
The other answers to this question offer up technical reasons why await Task.WhenAll(t1, t2, t3);
is preferred. This answer will aim to look at it from a softer side (which @usr alludes to) while still coming to the same conclusion.
await Task.WhenAll(t1, t2, t3);
is a more functional approach, as it declares intent and is atomic.
With await t1; await t2; await t3;
, there is nothing preventing a teammate (or maybe even your future self!) from adding code between the individual await
statements. Sure, you've compressed it to one line to essentially accomplish that, but that doesn't solve the problem. Besides, it's generally bad form in a team setting to include multiple statements on a given line of code, as it can make the source file harder for human eyes to scan.
Simply put, await Task.WhenAll(t1, t2, t3);
is more maintainable, as it communicates your intent more clearly and is less vulnerable to peculiar bugs that can come out of well-meaning updates to the code, or even just merges gone wrong.
Related videos on Youtube
avo
Updated on May 08, 2022Comments
-
avo almost 2 years
In case I do not care about the order of task completion and just need them all to complete, should I still use
await Task.WhenAll
instead of multipleawait
? e.g, isDoWork2
below a preferred method toDoWork1
(and why?):using System; using System.Threading.Tasks; namespace ConsoleApp { class Program { static async Task<string> DoTaskAsync(string name, int timeout) { var start = DateTime.Now; Console.WriteLine("Enter {0}, {1}", name, timeout); await Task.Delay(timeout); Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds); return name; } static async Task DoWork1() { var t1 = DoTaskAsync("t1.1", 3000); var t2 = DoTaskAsync("t1.2", 2000); var t3 = DoTaskAsync("t1.3", 1000); await t1; await t2; await t3; Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result)); } static async Task DoWork2() { var t1 = DoTaskAsync("t2.1", 3000); var t2 = DoTaskAsync("t2.2", 2000); var t3 = DoTaskAsync("t2.3", 1000); await Task.WhenAll(t1, t2, t3); Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result)); } static void Main(string[] args) { Task.WhenAll(DoWork1(), DoWork2()).Wait(); } } }
-
cuongle over 10 yearsWhat if you don't actually know how many tasks you need to do in parallel? What if you have 1000 tasks need to be run? The first one will be not much readable
await t1; await t2; ....; await tn
=> the second one is always the best choice in both case -
avo over 10 yearsYour comment makes sense. I was just trying to clarify something for myself, related to another question I recently answered. In that case, there were 3 tasks.
-
-
svick over 10 years“because it propagates all errors at once” Not if you
await
its result. -
usr over 10 years@svick hm that's true (and undesirable). I'm investigating this issue now: stackoverflow.com/questions/18314961/…
-
Oskar Lindberg over 10 yearsSorry for a late comment, but I just happened to "pass by", and I think the main reason to use WhenAll in favor of awaiting each task is related to performance. While awaiting each task separately effectively "serializes" the execution of the tasks, WhenAll has the potential of completing all tasks within the time-frame of the longest running single task, because they will be executed in parallel (if the executing environment supports it). I think the accepted answer should be edited to reflect this.
-
Oskar Lindberg over 10 yearsAs for the question of how exceptions are managed with Task, this article gives a quick but good insight to the reasoning behind it (and it just so happens to also make a passing note of the benefits of WhenAll in contrast to multiple awaits): blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx
-
usr over 10 years@OskarLindberg the OP is starting all tasks before he is awaiting the first one. So they run concurrently. Thanks for the link.
-
Oskar Lindberg over 10 years@usr I was curious still to know if WhenAll doesn't do clever things like conserving the same SynchronizationContext, to further push its benefits aside from the semantics. I found no conclusive documentation, but looking at the IL there are evidently different implementations of IAsyncStateMachine in play. I don't read IL all that well, but WhenAll at the very least appears to generate more efficient IL code. (In any case, the fact alone that the result of WhenAll reflects the state of all tasks involved to me is reason enough to prefer it in most cases.)
-
Servy over 10 yearsYou seem to think that sending a message tot he synchronization context is expensive. It's really not. You have a delegate that gets added to a queue, that queue will be read and the delegate executed. The overhead that this adds is honestly very small. It's not nothing, but it's not big either. The expense of whatever the async operations are will dwarf such overhead in almost all instances.
-
Marcel Popescu over 10 yearsAgreed, it was just the only reason I could think of to prefer one over the other. Well, that plus the similarity with Task.WaitAll where thread switching is a more significant cost.
-
careforapint almost 10 yearsAnother important difference is that WhenAll will wait for all tasks to complete, even if, e.g., t1 or t2 throws an exception or are canceled.
-
Chris Moschini over 9 years@Servy As Marcel points out that REALLY depends. If you use await on all db tasks as a matter of principle for example, and that db sits on the same machine as the asp.net instance, there are cases you'll await a db hit that's in-memory in-index, cheaper than that synchronization switch and threadpool shuffle. There could be a significant overall win with WhenAll() in that kind of scenario, so... it really depends.
-
Servy over 9 years@ChrisMoschini There is no way that the DB query, even if it's hitting a DB sitting on the same machine as the server, is going to be faster than the overhead of adding a few delegates to a message pump. That in-memory query is still almost certainly going to be quite a lot slower.
-
usr about 7 years@Magnus thanks, pulled into the answer. Very important.
-
usr about 7 years@OskarLindberg the task combinator helper functions don't do anything in particular with the sync context. They couldn't if they wanted to because the task producer decides what context to use.; Less await points mean less state machine code but I don't know that this would help performance since now WaitAll does a similar job and allocates memory. I think WaitAll is a tiny net performance loss. Not totally sure.
-
Maverick Meerkat almost 6 yearsAlso note that if t1 is slower and t2 and t3 are faster - then the other awaits return immediately.
-
Hari almost 5 years@usr The answer should probably be edited to clarify about error propagation upon await. I accidentally noted it in comments so would be better to add it to answer. Thanks for your good answer and following up on err propagation.
-
usr almost 5 years@Hari it actually was part of the answer as of your comment. But it was so badly written that I myself misunderstood it when rereading. It is better now.
-
D J almost 2 yearsThe question is not about http at all
-
markonius almost 2 yearsHi, welcome to the site. This may be true if the OP had awaited taskA before calling taskB, but they first started all the tasks.