0

I’m currently learning about C#, I’m wondering how Thread, Task, async and await really work. This is what I understand:

  • Task.Run(MyTask) is always executed on a new thread (in the thread pool)
  • Because of this, Task will run in parallel with the method that calls it (because they are on two different threads)
  • Therefore, use await to wait for the result of that Task, then continue executing the code below.
  • This is my example code:
static void Main(string[] args)
{
    Task.Run(MyTask);
    Console.WriteLine("Do main work...");
    Thread.Sleep(5000);
}

static async Task MyTask()
{
    await MyMiniTask();
    Console.WriteLine("Do Task...");
}

static async Task MiniTask()
{
    await Task.Delay(1000);
    Console.WriteLine("Do Mini Task...");
}

output:
Do main work...
Do Mini Task...
Do Task...
As I understand it, the code should run like this:

  • Run program -> Main thread is initialized to run the program
  • Task.Run(MyTask) -> thread A in the thread pool will handle MyTask
  • In the output, print out “Do main work…” first, even though the console writeline is written after Task.Run(MyTask). The reason is because Main thread and thread A are two different threads, so they will be executed in parallel
  • Back to thread A and MyTask, it encounters the statement: await MiniTask -> thread B in the thread pool will handle MiniTask (a new thread will always be used to handle MiniTask even if there is no await)
  • Because of using await, MyTask will have to wait for MiniTask to complete, and while waiting, thead A of MyTask does not handle anything -> MyTask will be marked as "a pause point" here and C# will release thread A back to the thread pool, thread A can now do another job
  • When MiniTask is completed, C# will return to "the pause point" of MyTask, and handle it with another thread in the ThreadPool (it could be thread A or any other free thread)


=> The meaning of using async await is: it will wait for the result of the asynchronous task (MiniTask), await keyword when used will release the thread of the method that calls it (Thread A of MyTask), and ThreadPool will allocate another thread to continue processing this method after MiniTask has completed.
There is a difference between using the Wait() method of Task and the keyword await, which is Wait() will not release the thread of the method that calls it (I have tried)
=>> Summary (according to my understanding):

  • When you want to perform multiple tasks in parallel without affecting the main thread -> use Thread, and now more modern is Task
  • When you want to wait for the result of a Task -> Wait() it
  • When you want to wait for the result of a Task, but want to release the Thread of the method that calls that Task in the waiting time -> await it


I don't know if I understand that right?
Sorry if my English is not good

This example code actually works and prints the output as above. But I don't know if it works the way I think or just a coincidence.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    Does this answer your question? [What is the difference between task and thread?](https://stackoverflow.com/questions/4130194/what-is-the-difference-between-task-and-thread) – Mohammad Aghazadeh Jul 23 '23 at 06:16
  • Bookmark this blog : https://blog.stephencleary.com/2014/04/a-tour-of-task-part-0-overview.html – Fildor Jul 23 '23 at 06:20
  • Just because you mentioned `Task.Wait` I have to add that you must use is with caution (it's usually avoidable). When a `Task` is created it captures the current `SynchronizationContext` of the caller and the continuation code (code after `await` or defined by the `Task.ContinueWith` delegate). This is required to enable the `Task` to execute the continuations. By default a `Task` is configured to execute the continuation on the caller's `SynchronizationContext` i.e. the caller's thread. This means `Task.ConfigureAwait` is implicitly configured with `Task.ConfigureAwait(true)`. –  Jul 23 '23 at 08:19
  • Unless you explicitly allow the `Task` to execute the continuation on the current thread by calling `Task.ConfigureAwait(false)` calling `Task.Wait` to wait for the `Task` object to run to completion will cause a deadlock. This is because, as you correctly mentioned, that `Task.Wait` will block the caller's thread until the `Task` has returned. This is effectively a common synchronous method invocation behavior. –  Jul 23 '23 at 08:19
  • Because of the default `Task.ConfigureAwait(true)` configuration the `Task` is forced to return to the caller's thread in order to continue to execute the continuation using the captured `SynchronizationContext`. But the caller's thread is busy with waiting for the `Task` to complete. So the `Task` has to wait until the calling thread is available to allow the `Task` to return, which can never happen because the caller's thread is blocked with waiting for the `Task`. That's the deadlock. –  Jul 23 '23 at 08:20
  • If you configure the `Task` to continue on a thread pool thread by calling `Task.ConfigureAwait(false)` the deadlock is lifted because the `Task` no longer has to return to the caller's thread and therefore `Task.Wait` won't stop the `Task` from completion. It's an important detail to understand. `Task.Wait`, Task.WaitAny` and `Task.WaitAll` have the same behavior in this regards. That's why you must always avoid the `Waitx` API and instead replace `Taks.Wait` ==> `await Task`, `Task.WaitAny`==> àwait Task.WhenAny` and `Task.WaitAll` ==> `await Task.WhenAll`. –  Jul 23 '23 at 08:20
  • It's also best practice for library code that is not expected to run in an UI environment to always configure *every* `Task` using `Task.ConfigureAwait(false)` to prevent this deadlock from happening in the client code (and of course to improve the performance by eliminating the `SynchronizationContext` context switch). –  Jul 23 '23 at 08:20
  • 1
    @BionicCode. Thank you for your reply, it was very helpful – Hai Nguyen Jul 23 '23 at 09:00

1 Answers1

2

Your understanding is mostly OK. One point that needs to be corrected is here:

Task.Run(MyTask) is always executed on a new thread (in the thread pool)

Not on a new thread. The whole point of having a ThreadPool is to reuse a small number of threads, in order to avoid paying the cost of creating a new thread each time a thread is needed to do some minuscule work. So it's just a thread, not a new thread.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104