3

I wrote a really simple check that tells me what thread I am on and then added some async await code. I noticed that the first time I check I am on thread1, then thread3, and I never return to thread1 during my code execution.

Can anyone explain to me why after the await I don't return to the main thread that called await? The output of WhatThreadAmI() goes as follows:

********************** Main - 17 -- True ********************** ********************** CountAsync - 37 -- False ********************** ********************** Main - 22 -- False ********************** ********************** Main - 29 -- False ********************** 

Example code:

class Program { static Thread mainThread; public static async Task Main(string[] args) { mainThread = Thread.CurrentThread; WhatThreadAmI(); Console.WriteLine("Counting until 100 million in 5 seconds ..."); var msWait = await CountAsync(); WhatThreadAmI(); Console.WriteLine($"Counting to 100 million took {msWait} milliseconds."); Console.WriteLine("Press any key to exit"); Console.ReadKey(); WhatThreadAmI(); } static async Task<String> CountAsync() { return await Task.Run(() => { WhatThreadAmI(); Task.Delay(TimeSpan.FromSeconds(5)).Wait(); var startTime = DateTime.Now; var num = 0; while (num < 100000000) { num += 1; } return (DateTime.Now - startTime).TotalMilliseconds.ToString(); }); } static void WhatThreadAmI([CallerMemberName]string Method = "", [CallerLineNumber]int Line = 0) { const string dividor = "**********************"; Debug.WriteLine(dividor); Debug.WriteLine($"{Method} - {Line} -- {IsMainThread()}"); Debug.WriteLine(dividor); } public static bool IsMainThread() => mainThread == Thread.CurrentThread; } 
8
  • 4
    And why do you think it must return there? Commented Nov 29, 2017 at 20:03
  • Await tells the compiler to start on a thread, jump to another thread (skip code and continue executing), return to caller thread. Commented Nov 29, 2017 at 20:04
  • My understanding is that a state machine is created I don't know if that necessarily means that it goes back to the original thread. Commented Nov 29, 2017 at 20:06
  • @BaileyMiller None of what you said is true, await tells the compiler "Check to see if the task is complete, if so run the continuation on the calling thread. If not schedule the continuation using the current OperationContext, if no current OperationContext schedule it using the default scheduler (which is the thread pool) Commented Nov 29, 2017 at 20:07
  • @ScottChamberlain but if there was an OperationContext in effect you return to the caller thread? Commented Nov 29, 2017 at 20:09

2 Answers 2

3

await will capture current synchronization context (SynchronizationContext.Current) and post continuation (everything after await) to that context (unless you use ConfigureAwait(false)). If there is no synchronization context, like in console application (your case) - by default continuation will be scheduled to thread pool thread. Your main thread is not thread pool thread, so you will never return to it in code you posted.

Note that every synhronization context implementation can decide what to do with callbacks posted to it, it does not necessary for it to post callback to single thread (like WPF\WinForms synchronization contexts do). So even with synchronization context you are not guaranteed to "return back to caller thread".

Sign up to request clarification or add additional context in comments.

5 Comments

I was always under the impression I returned to my main thread, or essentially my UI thread. I mainly code Wpf applications and that is how I typically think of my threading. So with my current code I will start with 1 thread, spawn another and for the life time of my console have 2 threads when my original thread is just sleeping.
@BaileyMiller yes. doing a async main is just a wrapper for a function that looks like static void RealMain(string[] args) { Main(args).GetAwaiter().GetResult();} which gives the behavior of having the "main" thread sleeping for the entire program and thread pool threads doing all the work.
@BaileyMiller In WPF you returned to main thread indeed, but that's in WPF, because there is specific SynchornizationContext there. As for threads - not exactly, because thread pool manages threads in its own way. It will create and destroy threads as necessary. Task.Run does not necessary create new thread, pool might already have it available for you. And pool with destroy threads which are not used for a long time. But your main thread will always be there of course, mostly sleeping in this case.
I have never looked further into how the api handles threading in C# and didn't realize there was so much of a difference between the C# applications. Thanks for the answer and the discussion in comments.
"by default continuation will be scheduled to thread pool thread." -- That's not how it works. In the absence of a synchronization context, the continuation runs on whatever thread completed the task (demo). It just happens that all built-in asynchronous APIs in .NET complete their tasks on ThreadPool threads, creating this common illusion.
0

Even though you can now have async main functions there still no OperationContext for console applications. Because of that it will use the default task scheduler which will cause continuations after an await to use any thread pool thread instead of the original thread associated with the OperationContext.

2 Comments

So if I kept using await and spawned more threads I would in essence be spawning threads that get placed into the wait state while other threads are working? My main thread is sitting in a WaitSleepJoin ThreadState.
The only thing that "spawns" threads is the Task.Run you have in your code. await does not create threads, it just waits for tasks to enter the completed state then runs a continuation of the code after the await using the OperationContext (which in your case will be queueing work to the thread pool)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.