multiple awaits vs Task.WaitAll - equivalent?

33,155

Solution 1

The first option will not execute the two operations concurrently. It will execute the first and await its completion, and only then the second.

The second option will execute both concurrently but will wait for them synchronously (i.e. while blocking a thread).

You shouldn't use both options since the first completes slower than the second and the second blocks a thread without need.

You should wait for both operations asynchronously with Task.WhenAll:

public async Task<IHttpActionResult> MethodB()
{
    var customer = new Customer();

    var getAllWidgetsTask = _widgetService.GetAllWidgets();
    var getAllFoosTask = _fooService.GetAllFos();

    await Task.WhenAll(getAllWidgetsTask, getAllFoosTask);

    customer.Widgets = await getAllWidgetsTask;
    customer.Foos = await getAllFoosTask;

    return Ok(customer);
}

Note that after Task.WhenAll completed both tasks already completed so awaiting them completes immediately.

Solution 2

Short answer: No.

Task.WaitAll is blocking, await returns the task as soon as it is encountered and registers the remaining part of the function and continuation.

The "bulk" waiting method you were looking for is Task.WhenAll that actually creates a new Task that finishes when all tasks that were handed to the function are done.

Like so: await Task.WhenAll({getAllWidgetsTask, getAllFoosTask});

That is for the blocking matter.

Also your first function does not execute both functions parallel. To get this working with await you'd have to write something like this:

var widgetsTask = _widgetService.GetAllWidgets();
var foosTask = _fooService.GetAllWidgets();
customer.Widgets = await widgetsTask;
customer.Foos = await foosTask;

This will make the first example to act very similar to the Task.WhenAll method.

Share:
33,155
vidalsasoon
Author by

vidalsasoon

Updated on July 05, 2022

Comments

  • vidalsasoon
    vidalsasoon almost 2 years

    In terms of performance, will these 2 methods run GetAllWidgets() and GetAllFoos() in parallel?

    Is there any reason to use one over the other? There seems to be a lot happening behind the scenes with the compiler so I don't find it clear.

    ============= MethodA: Using multiple awaits ======================

    public async Task<IHttpActionResult> MethodA()
    {
        var customer = new Customer();
    
        customer.Widgets = await _widgetService.GetAllWidgets();
        customer.Foos = await _fooService.GetAllFoos();
    
        return Ok(customer);
    }
    

    =============== MethodB: Using Task.WaitAll =====================

    public async Task<IHttpActionResult> MethodB()
    {
        var customer = new Customer();
    
        var getAllWidgetsTask = _widgetService.GetAllWidgets();
        var getAllFoosTask = _fooService.GetAllFos();
    
        Task.WaitAll(new List[] {getAllWidgetsTask, getAllFoosTask});
    
        customer.Widgets = getAllWidgetsTask.Result;
        customer.Foos = getAllFoosTask.Result;
    
        return Ok(customer);
    }
    

    =====================================

  • vidalsasoon
    vidalsasoon over 8 years
    Thanks. This is what I needed. The ".Result" was bothering me and your answer avoids that.
  • i3arnon
    i3arnon over 8 years
    @vidalsasoon sure.. any time.
  • kwesolowski
    kwesolowski over 8 years
    You can also completely skip await Task.WhenAll(getAllWidgetsTask, getAllFoosTask); and just await tasks (just start second task before awaiting first).
  • i3arnon
    i3arnon over 8 years
    @kwesoly that's true, but it's a little bit more efficient to "suspend" only once.
  • kwesolowski
    kwesolowski over 8 years
    @i3arnon - sounds as good reason, there is really no point to switch context just to await again.
  • Stephen Holt
    Stephen Holt over 5 years
    This seems a little inelegant, as we're using "await" on the two tasks after we already know they've completed. What is wrong with just using the .Result property?
  • i3arnon
    i3arnon over 5 years
    @StephenHolt what's wrong with using await? await doesn't care whether the task is completed or nor (it even optimizes if it is).
  • Servy
    Servy over 5 years
    @StephenHolt There's exactly one difference between the two. Result wraps any exceptions in an AggregateException, which you then need to unwrap in all of your error handling code. await unwraps it for you. Other than that they're identical.
  • Stephen Holt
    Stephen Holt over 5 years
    @i3arnon no reason I guess... I'd just never seen await used in that context before, and I just thought async implied something that should take a while to complete. Thanks to yourself and Servy for explaining.
  • Theodor Zoulias
    Theodor Zoulias almost 4 years
    This a good answer, but it should be noted that the proposed solution has a "disadvantage" over the original example with Task.WaitAll. The disadvantage is that in case both internal tasks fail (getAllWidgetsTask and getAllFoosTask), only the exception of the first task is going to be propagated. On the contrary the original Task.WaitAll will propagate both exceptions wrapped in an AggregateException.
  • i3arnon
    i3arnon almost 4 years
    @TheodorZoulias Task.When returns a task. If the task will be faulted its Exception property will hold an AggregateException with all the inner exceptions. await only rethrows the first, but you can catch it and look at the task for the rest.
  • Theodor Zoulias
    Theodor Zoulias almost 4 years
    Yeap, indeed. But the method MethodB is not doing any error handling internally. It just propagates a possible exception to the caller. And by using await it silently discards all exceptions except from the first. I am not saying that this is hugely important. I am just pointing out to a difference between the two implementations, that is not obvious at first glance, and may have significance in some cases.
  • harry
    harry over 2 years
    @TheodorZoulias has a very important point, but the only advantage not mentioned here to starting both tasks then awaiting separately is that it allocates (much) less. I'm sure as the number of tasks goes up, it'll even out then maybe weigh in Task.WhenAll's favor, but with only two tasks, awaiting them separately allocates a good deal less. See benchmark here: dotnetfiddle.net/hcd6Jb
  • Theodor Zoulias
    Theodor Zoulias over 2 years
    @Mahmoud Al-Qudsi my comment was about the difference between Task.WaitAll and await Task.WhenAll, not between await Task.WhenAll and awaiting each task individually. Individually awaiting each task has serious repercussions that make any benchmark results mostly irrelevant. If the first task fails, the second task will be leaked as a fire-and-forget task. This is rarely what you want. In most cases you don't want to lose track of your tasks, and have them running rampant out of control.
  • harry
    harry over 2 years
    Oh, I know - that’s why I said you had a good point (in my case, each task had a continuation running OnlyOnFaulted that handled the fallout).
  • Theodor Zoulias
    Theodor Zoulias over 2 years
    @Mahmoud Al-Qudsi are you awaiting those OnlyOnFaulted continuations, or are themselves fire-and-forget?