-3

Is there a different between

    var taskA = GetObjectA();
    var taskB = GetObjectB();
    var taskC = GetObject3();

    await Task.WhenAll(taskA, taskB, taskC);

    return new AllTasksResponse
    {
        A = taskA.Result,
        B = taskB.Result,
        C = taskC.Result
    };

and

return new AllTasksResponse
{
    A = await GetObjectA(),
    B = await GetObjectB(),
    C = await GetObjectC()
};

?

Basically, I want to know if creating a new object that awaits for multiple tasks to finish runs them asynchronously. Or should I use WhenAll (or WaitAll) in these cases to ensure that all tasks will run in parallel?

Alexandre Paiva
  • 304
  • 3
  • 13
  • 3
    There's a significant difference: the first runs in parallel, the second sequentially. Which one you should use is up to the concrete scenario – Camilo Terevinto Oct 05 '20 at 18:12
  • 1
    Yes, there's a huge difference between starting three tasks and then passing them to `WhenAll()`, as compared to starting and awaiting each task individually, one at a time. See duplicate. (Note: even when you use `WhenAll()`, it is better to use `await` to retrieve the result, rather than the `Result` property.) – Peter Duniho Oct 05 '20 at 18:19
  • The problem with the duplicate is that the difference is very clear when you're awaiting multiple tasks in sequence. But in this case I wanted to know if using "new Object()" (or even return) changes that scenario. I wasn't sure if they would still run in order and not in parallel. – Alexandre Paiva Oct 05 '20 at 18:29
  • _"the difference is very clear when you're awaiting multiple tasks in sequence"_ -- that's exactly what you're doing here. Why you think that's different at all from the identical question in the duplicate, I have no idea. The "new Object" is just a red herring. It has _nothing_ to do with anything related to async/await. It's just syntactic sugar for creating the object and then assigning the three properties in sequence. That the sequence is inside braces instead of as individual program statements is irrelevant. – Peter Duniho Oct 05 '20 at 18:30
  • I was wondering if '''new''' had this particularity for async methods. As you have to await multiple tasks to return a new object, it would make sense to, internally, wait for them in parallel. But as you've explained it's just syntatic sugar. – Alexandre Paiva Oct 05 '20 at 18:40
  • 1
    @Theodor: I beg to differ. While the questions are presented differently, the answer to the original duplicate addresses this scenario head-on. Still, I've updated the duplicates to include one that you should find more direct. – Peter Duniho Oct 05 '20 at 21:36

1 Answers1

2

Yes, there's enormous difference between the two.

  1. In the first case, all 3 tasks will proceed in parallel and independently assuming there's no resource contention among them.

    However, in the second case they'll only proceed one by one.

  2. The remarks section for WhenAll() is also significant.

  3. Depending upon the synchronization context in effect, if any at all, the point where you experience a potential deadlock is also different for both.

  4. The point where an exception thrown by the task is visible differs.

Tanveer Badar
  • 5,438
  • 2
  • 27
  • 32
  • *"the thread upon which that method was first called will/might be free to do something else."* <=== Is this true only for the second approach, or is it true for the first approach too? – Theodor Zoulias Oct 05 '20 at 18:20
  • True for both, each `await` is an opportunity for your thread to be switched when the underlying continuation resumes. This again depends upon the synchronization context in use. This cannot happen if `await` completes synchronously. – Tanveer Badar Oct 05 '20 at 18:22
  • Then why did you made it a bullet in the list of differences between the two? – Theodor Zoulias Oct 05 '20 at 20:36
  • Which one of those four bullets? – Tanveer Badar Oct 06 '20 at 04:25
  • The bullet number 1. – Theodor Zoulias Oct 06 '20 at 05:40
  • The difference is how the tasks proceed in the two versions. In the first one you have three - possibly independent, like I note in the answer itself - units of work and the single `await Task.WhenAll()` will act like a barrier, in a manner of speaking, to force completion of participants. OTOH, the second version barely has any apparent concurrency and only one task at a time can proceed since you don't even start another until the previous one `RanToCompletion` of sorts, which brings me to another point I should add to the answer. – Tanveer Badar Oct 06 '20 at 06:43
  • 2
    I am focusing on the claim that the calling thread is freed during the awaiting, to do other work. This claim is correct, but it is true for both approaches (concurrent and sequential). So why is it there, in a list of differences? My suggestion would be to remove it from the list. – Theodor Zoulias Oct 06 '20 at 06:50
  • 1
    Now I understand your point, thank you. Let me see if I can rephrase that in a better manner. – Tanveer Badar Oct 06 '20 at 06:52
  • 1
    The exception is no different in either case. `await` takes the first inner exception from the aggregate exception, so even in that case the same exception is observed. – Servy Oct 06 '20 at 13:25
  • `await` picks one exception with undocumented logic out of the many to serve as top level exception for the `AggregateException`, all of the exceptions again available in the `InnerExceptions` property. So yes, you get different behavior in both cases. – Tanveer Badar Oct 06 '20 at 15:27
  • 2
    @TanveerBadar No, it doesn't. You can run the following code and see that there is only ever one inner exception, not two, even though `WhenAll` was given two tasks that both faulted. If you make Foo not `async` and just return what `WhenAll` returns, *then* you get two exceptions. You need to not use `await` to keep them though. `async Task Foo() => await Task.WhenAll(Task.FromException(new Exception("First")), Task.FromException(new Exception("Second"))); var task = Foo(); Console.WriteLine(task.Exception.InnerExceptions.Count);` – Servy Oct 06 '20 at 16:04
  • @Servy The [example](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/try-catch#taskwhenall-example) in docs for this exact case has different output, upon which I based my observation. Let me run both examples as soon as I can. – Tanveer Badar Oct 06 '20 at 18:20
  • 1
    @TanveerBadar In that example they're not inspecting the caught exception, they're inspecting the task returned by `WhenAll` before it was awaited. So, again, they're subverting the exception behavior of `await` precisely because it's not what they want. In that toy example it's manageable, because the exception is being caught in the method that has the `WhenAll`, but if you're throwing the exception further up the stack, getting that task because *much* harder. And of course in the example in the question the task is never stored in any variable, it's *only* awaited. – Servy Oct 06 '20 at 18:36
  • 1
    So the behavior of the two samples in the OP are indeed the same with respect to exceptions thrown, as has been said. – Servy Oct 06 '20 at 18:36
  • @Servy That is so incredibly stupid behavior on `await`'s part, I am astonished. Thank you, I learned something new today. – Tanveer Badar Oct 06 '20 at 18:53
  • @TanveerBadar Meh, `await` is compensating for the incredibly stupid behavior of `Task.Result` and `Task.Wait` that always wrap every exception in an `AggregateException`. `await` tried to compensate for it by unwrapping the aggregate exception. If you've done much task related coding before `await` was a thing, you'd realize it unwrapping the aggregate exceptions is very helpful. Perhaps it could have only unwrapped aggregates with only one exception, but then it becomes hard to predict. WhenAll could *double* wrap the exceptions in AggregateExceptions, but then that's super confusing... – Servy Oct 06 '20 at 19:11