Quite often, people think that asynchronous waiting is performed by multiple threads. In fact, all this is done in one thread.
See the appendix below about this single thread.
What really helped me understand asynchronous waiting is this interview with Eric Lippert about asynchronous waiting . Somewhere in the middle, he compares the asynchronous wait with the cook, who must wait for the water to boil. Instead of doing nothing, he looks around to see if there is anything else, such as chopped onions. If this is finished and the water still does not boil, he checks to see if there is anything else, and so on, until he has nothing to do but wait. In this case, he returns to the first that he was waiting.
If your procedure calls the expected function, we are sure that somewhere in this expected function there is a call to the expected function, otherwise the function will not be expected. In fact, your compiler will warn you if you forget to wait somewhere in your expected function.
If your expected function calls another expected function, then the thread enters this other function, begins to perform actions in this function, and delves into other functions until it encounters an expectation.
Instead of waiting for the results, the thread goes up the call stack to see if there are any other pieces of code that it can process until it sees the wait. Rise again in the call stack, process until you wait, etc. As soon as everyone expects, the thread searches for the lower expectation and continues as soon as it ends.
This has the advantage that if the caller of your expected function does not need the result of your function, but can do other things before the result is needed, these other things can be executed by the thread instead of waiting inside your function.
A call that does not expect an immediate result will look like this:
private async Task MyFunction() { Task<ReturnType>taskA = SomeFunctionAsync(...)
Note that in this scenario, everything is executed by a single thread. As long as your topic has something to do, it will be busy.
Addition. It is not true that only one thread is involved. Any thread that has nothing to do can continue to process your code after waiting. If you check the thread identifier, you will see that this identifier can be changed after waiting. The continuing stream has the same context as the original stream, so you can act as if it were the original stream. No need to check InvokeRequired , no need to use mutexes or critical sections. For your code, it is as if one thread was involved.
The article link at the end of this answer explains a little more about the context of the stream.
You will see the expected functions mainly where some other process has to do something, while your thread just has to wait until the other thing is complete. For example, sending data over the Internet, saving a file, connecting to a database, etc.
However, sometimes you need to perform some complex calculations, and you want your thread to be free to do something else, for example, respond to user input. In this barrel, you can run the expected action as if you had called an asynchronous function.
Task<ResultType> LetSomeoneDoHeavyCalculations(...) { DoSomePreparations()
Now another thread is doing heavy calculations, while your thread can do other things. As soon as he starts to wait, your caller can do something until he starts to wait. Effectively, your theme will respond fairly freely to user input. However, this will only take place if everyone is waiting. While your thread is busy with things, it cannot respond to user input. Therefore, always make sure that if you think your user interface thread should do some busy processing that takes some time, use Task.Run and let the other thread do this
Another article that helped me: Async-Await from brilliant explanator Stephen Cleary