1

I was reading an article by @stephen-cleary around async/await. He mentioned below code

public async Task DoOperationsConcurrentlyAsync()
{
  Task[] tasks = new Task[3];
  tasks[0] = DoOperation0Async();
  tasks[1] = DoOperation1Async();
  tasks[2] = DoOperation2Async();

  // At this point, all three tasks are running at the same time.

  // Now, we await them all.
  await Task.WhenAll(tasks);
}

He mentions the tasks are already started when the methods are called (e.g DoOperation0Async()). In which thread does these methods gets run in? Do all methods that return a Task run in a different thread (or get queued up in the threadpool).

OR do they all run synchronously? If they run synchronously, why use Task.WhenAll() ? Why not just await each method that returns a Task?

public async Task DoOperationsConcurrentlyAsync()
{
  await DoOperation0Async();
  await DoOperation1Async();
  await DoOperation2Async();
}
stuck_inside_task
  • 273
  • 1
  • 2
  • 6
  • 4
    Task should not be confused with Threads, and they should not be confused with Parallelism. Tasks are for asynchronous operations, that do not block your execution Thread. For example I/O bound tasks wouldn't have any Thread usage at all (only continuation will be scheduled on Thread Pool). CPU bound tasks would most likely be scheduled on the Thread Pool for execution. There is a chance that there will be no available threads for execution then, when you await, the Task will be executed by the calling thread (that's the only case when it will be synchronous execution) – Nikita Chayka Oct 31 '22 at 18:53
  • 2
    ^^ this is important to understand. A task can be a wrapper for an operation that is supported natively by the operating system, e.g. reading from a file. In this case, the OS is capable of executing the requested operation in the background and notifying the C# runtime when complete. It does not consume a thread. _Continuation_ - what you do after `await`ing the task - is executed on a thread. – Andrew Williamson Oct 31 '22 at 19:02
  • 4
    Stephen Cleary has an article describing this in better detail than I can: [There Is No Thread](https://blog.stephencleary.com/2013/11/there-is-no-thread.html) – Andrew Williamson Oct 31 '22 at 19:04
  • 1
    The asynchronous methods are running synchronously in the sense that the `Task` objects are created synchronously, on the current thread. Each task is created after the other, sequentially. But an asynchronous method is more than just the `Task` object. It is also the operation that is represented by the `Task` object. Maybe this is the point that you are missing. The common coding pattern `await method()` conflates the creation and the awaiting of the `Task`, confusing the beginners in async/await. The operation represented by a `Task` is said to be "in-flight", until the task is completed. – Theodor Zoulias Oct 31 '22 at 20:01
  • Note that method can return Task and still run completely synchronously, e.g. `return Task.CompletedTask`. – Evk Oct 31 '22 at 20:02

2 Answers2

0

In the second code you proposed, You didn't start to run DoOperation1Async until DoOperation0Async is finished. And DoOperation2Async Will not start to run until DoOperation1Async. They will run in a sequential way.

On the first code block, They all run in the same time (Depends on the number of CPU) And the WhenAll will release after they are all completed.

You can search for more examples here.

Ygalbel
  • 5,214
  • 1
  • 24
  • 32
0

In which thread does these methods gets run in? Do all methods that return a Task run in a different thread (or get queued up in the threadpool).

They will be queued, so the same thread could potentially run many of those tasks. This will vary based on how many threads are available at that time, and how fast each of those calls execute. For example, if the actions are trivially simple, it's possible that by the time you queue the third call, the first has already finished and the same thread that executed it could then execute the third one.

In this scenario, the await at the end would potentially only be a way to "communicate the task's result" back to the caller. It all depends on how fast the operations are executed.

In a normal flow, the await will also stop the caller thread and wait until the task results are ready, then a thread will be notified and continue the execution past that point (that's why it's called a continuation).

OR do they all run synchronously? If they run synchronously, why use Task.WhenAll() ? Why not just await each method that returns a Task?

Awaiting each individual call will guarantee that they are executed in sequence, but calling them without await and using Task.WhenAll at the end will ensure there is an attempt to use as much parallelism as possible.

I believe in some extremely constrained scenario, where you only have a single execution thread, that they would end up being equivalent: in that case, the same thread who called would have to be used to actually execute the actions so they would be forced to be executed sequentially. That's an extreme corner case however.

julealgon
  • 7,072
  • 3
  • 32
  • 77
  • *"using Task.WhenAll at the end will ensure there is an attempt to use as much parallelism as possible"* No it isn't. It depends on your scheduler and schronization context, and it doesn't really say anything about parallelism at all. All `WhenAll` does is queues up a continuation that runs when *all* of the tasks have completed. It makes no guarantee as to how those tasks already scheduled are being run. And normally a proper `async` implementation will not use *any* threads, it will simply post an IO completion routine and suspend the task. – Charlieface Nov 01 '22 at 13:11
  • @Charlieface I'm not saying that the `Task.WhenAll` call "enables" parallelism itself, I'm saying the "approach using it at the end with the tasks upfront without `await`" will in most cases result in parallel execution. – julealgon Nov 01 '22 at 13:16