1

I do not understand how is the control returned to the caller when using async- await, since when i execute this code, the first thread gets practically destroyed when calling task inside the awaited method, and the thread that gives the result executes all remaining code.Below i have also drawn a diagram of how i thought the execution is, but it seems it is wrong.

Assumed workflow according to "returning control to the caller":

Results

Results

enter image description here

Main

           public static string GetThreadId => Thread.CurrentThread.ManagedThreadId.ToString();

           static async Task Main(string[] args) {
                Console.WriteLine("From main before async call , Thread:" + GetThreadId);

                string myresult = await TestAsyncSimple();

                Console.WriteLine("From main after async call ,Thread:" + GetThreadId);
                Console.WriteLine("ResultComputed:" + myresult+",Thread:"+GetThreadId);
                Console.ReadKey();

            }

Async Task

         public static async Task<string> TestAsyncSimple() {

            Console.WriteLine("From TestAsyncSimple before delay,Thread:" + GetThreadId);
            string result=await Task.Factory.StartNew(() => {
                Task.Delay(5000);
                Console.WriteLine("From TestAsyncSimple inside Task,Thread:" + GetThreadId);
                return "tadaa";
                });
            Console.WriteLine("From TestAsyncSimple after delay,Thread:" + GetThreadId);
            return result;
           }

Can anyone point me to the right direction?Also what causes the new thread to get spawned?Always when starting a Task ?Are there other "triggers" besides tasks that create new threads which will execute the remaining code?

Bercovici Adrian
  • 8,794
  • 17
  • 73
  • 152
  • 3
    No new thread is spawned. Control isn't returned to the caller. The calling code resumes execution on the original thread/synchronization context after `await`. In desktop applications they are the same thing – Panagiotis Kanavos Feb 14 '18 at 11:07
  • 2
    In *console* applications there's no UI thread so execution can resume in any threadpool thread. – Panagiotis Kanavos Feb 14 '18 at 11:09
  • As you can see in the results the code in main after the await line is executed after the result is computed and on another thread. – Bercovici Adrian Feb 14 '18 at 11:12
  • For better understanging I'd suggest to not use `async Task Main` and just use regular `void Main`. – Evk Feb 14 '18 at 11:16
  • But still why is the initial thread destroyed after reaching the Task.Run inside the awaited method? – Bercovici Adrian Feb 14 '18 at 11:23
  • Why do you think that a Thread is being destroyed? – H H Feb 14 '18 at 11:32
  • 2
    @user1913744 yes, another threadpool thread because there's no UI thread. No new thread is created, a thread is picked from the threadpool. In a *Winforms* or *WPF* application though, execution would be resumed on the UI thread. In fact, if that thread was blocked for another reason, the application could deadlock – Panagiotis Kanavos Feb 14 '18 at 11:33
  • @user1913744 threads are *not* being created or destroyed. *Task* are executed using threads from a threadpool. When a task finishes, the thread is put back in the threadpool and can be reused to execute the next task. – Panagiotis Kanavos Feb 14 '18 at 11:34
  • Well i think i should rewrite to : Why is the initial thread not continuing doing work in the main method?Why did Task.Run(lambda) cause him to "dissapear"?And by disappear i mean destroyed or any other state besides running. – Bercovici Adrian Feb 14 '18 at 11:34
  • If I get up from my chair and sit down in another one, my original chair isn't destroyed. It just happens to be the case that nobody's sitting in it at the moment. Also, whether or not I choose to continue to sit in my chair or go to another one would depend on circumstances. Now replace "chair" with "worker thread" and "me" with "useful work" and you have roughly your situation in a nutshell. It is legal, and possible, for an asynchronous task to continue running to the same thread it originally started on. But most schedulers don't require it. – Jeroen Mostert Feb 14 '18 at 11:39
  • @user1913744 it doesn't. It isn't destroyed. I'll repeat it, in WinForms or WPF execution *does* resume on the main thread, because only that thread can modify the UI. In a console application there's no UI so there's no need to resume on the original thread. A thread is picked from the pool. – Panagiotis Kanavos Feb 14 '18 at 11:39
  • 2
    Part of your confusion probably stems from the fact that await *does not wait*. It's a convenient way to write asynchronous code. The whole method after "await" is packed up as a continuation, shipped off to the asynchronous task and eventually executed on whatever thread becomes available. The "original thread" becomes irrelevant in this way, and the reason it doesn't continue running what it's running is because there's nothing left to run. – Jeroen Mostert Feb 14 '18 at 11:40
  • 1
    @user1913744 `async Task Main` though is a bit of a compiler trick. The compiler creates something like `Main(args)).GetAwaiter().GetResult()`. You can say that the original thread is still there, blocked and waiting for the `async Task Main()` function to complete – Panagiotis Kanavos Feb 14 '18 at 11:42
  • 1
    @user1913744 you can see all this visually if you profile your application using Visual Studio and the [Concurrency Visualizer](https://marketplace.visualstudio.com/items?itemName=VisualStudioProductTeam.ConcurrencyVisualizer2017) – Panagiotis Kanavos Feb 14 '18 at 11:44
  • 1
    `Task.Delay(5000)` is doing nothing because you are not awaiting it the result – Gusdor Feb 14 '18 at 11:54
  • 3
    "Destroyed" is the wrong mental model. The main thread of a console mode app is fairly special. When it completes, finishing executing the Main() entrypoint, then the CLR has a good reason to complete the program as well. Only another thread with its IsBackground property set to *false* could prevent that. You don't have one. You need to realize that async/await just isn't useful in this case. That main thread isn't doing anything useful, other than quitting, it might as well keep busy running the await code. – Hans Passant Feb 14 '18 at 12:01
  • @JeroenMostert The thing i don't understand is why isn't the code after the await method executed by thread 1.Since it is supposed to be "async" as you said ..i ship the long running method to a task on whatever thread available so i can continue with thread 1 doing the next instructions. – Bercovici Adrian Feb 14 '18 at 12:34
  • 1
    Because that's not what you asked for, from the compiler's point of view. If you wanted that, you wouldn't `await` the task, you'd store the resulting `Task` in a variable, do whatever you want while the task is going, and then `await` it when you need the result. – Jeroen Mostert Feb 14 '18 at 12:42
  • Well i thought that await Task or Task().Result are the same thing.They will return the value to the variable assigned. – Bercovici Adrian Feb 14 '18 at 12:44
  • 1
    Yes -- but `await` rewrites the rest of the method as a continuation, while (synchronously) invoking `.Result` does not. It looks very much the same to you, but that's the magic of `await`: it hides a whole bunch of compiler magic under the covers. It allows you to write asynchronous code *as if* it was synchronous, but it's not. – Jeroen Mostert Feb 14 '18 at 12:47
  • 1
    @user1913744 Can you do yourself a huge favour and dive into one of the _many_ blogs that introduce async/await? I think it would help you much more than this question https://blog.stephencleary.com/2012/02/async-and-await.html – Gusdor Feb 14 '18 at 12:56

1 Answers1

5

async Main method is converted to something like this:

static void Main() {
    RealMain().GetAwaiter().GetResult();
}

static async Task RealMain() {
    // code from async Main
}

With that in mind, at "From main before async call" point you are on main application thread (id 1). This is regular (non thread pool) thread. You will be on this thread until

await Task.Factory.StartNew(...)

At this point, StartNew starts a new task which will run on a thread pool thread, which is created or grabbed from pool if already available. This is thread 3 in your example.

When you reach await - control is returned back to the caller, where caller in this case is thread 1. What this thread does after await is reched? It's blocked here:

 RealMain().GetAwaiter().GetResult();

waiting for result of RealMain.

Now thread 3 has finished execution but TestAsyncSimple() has more code to run. If there were no synchronization context before await (the case here - in console application) - the part after await will be executed on available thread pool thread. Since thread 3 has finished execution of its task - it is available and is capable to continue execution of the rest of TestAsyncSimple() and Main() functions, which it does. Thread 1 all this time is blocked as said above - so it cannot process any continuations (it's busy). In addition it's also not a thread pool thread (but that is not relevent here).

After you reached Console.ReadKey and pressed a key - Main task finally completes, thread 1 (waiting for this task to complete) is unblocked, then it returns from real Main function and process is terminated (only at this point thread 1 is "destroyed").

Evk
  • 98,527
  • 8
  • 141
  • 191
  • So basically control is not returned back to Thread1, because it is "non-thread pool" thread. In case of WPF/WinForms in the beginning there is always one thread pool thread (`UI` thread), so the control can be returned back to this thread. Do I understand this correctly? – FCin Feb 14 '18 at 12:07
  • @FCin not exactly. Control cannot return back to Thread1 because it's blocked by waiting for the task to complete (as described). If WPF\WinForms there is synchronization context, so by default continuation of async function will run on UI thread (which is not thread pool thread). So, if before `await` there is sync context - continuation runs on it, if not (console application, or just any non-UI thread) - continuation runs on thread pool thread. – Evk Feb 14 '18 at 12:15
  • Well i thought that thread 1 will ship the async method with its result to whoever is available on a task so it can continue doing work in main (the code after the await).In our case printing.I thought that the code that will use the result will be done by thread 3 and the code which doesn't will be done by thread 1.And if you want to merge them you use Wait or WhenAll or some construct similar. – Bercovici Adrian Feb 14 '18 at 12:36
  • @FCin so besides UI i can not return the control to a caller in an enclosing scope?Check my drawing.So there is no possible way i can continue doing stuff concurrently on Main while the task is being run on another thread.Or not with async/await at least. – Bercovici Adrian Feb 14 '18 at 12:42
  • @user1913744 if "so it can continue doing work in main (the code after the await)" - for such model of execution you don't need await. If you just start task and forget about it (so - will not use await) - that's exactly what would happen. – Evk Feb 14 '18 at 12:43
  • 3
    @Evk Ok, so there are 2 reasons why it cannot return control back to the caller. First is that `Thread1` is waiting for `Thread3` to finish and so is busy. The second one is that `Thread1` does not have `Synchronization Context` because this is a Console App. In WPF/Winforms there is synchronization context and so `Thread3` would want to return control, but the `Thread1` would be busy waiting, so we would have a deadlock. Is that correct? – FCin Feb 14 '18 at 13:39
  • @FCin yes that's correct. That often happens when you do something like `SomeAsyncFunction().Wait()` from UI thread, blocking it (so just like in this question, if thread 1 were UI thread). – Evk Feb 14 '18 at 13:42