Async/Await Different Thread Id

async/await different thread ID

I recommend you read my async intro post for an understanding of the async and await keywords. In particular, await (by default) will capture a "context" and use that context to resume its asynchronous method. This "context" is the current SynchronizationContext (or TaskScheduler, if there is no SynchronzationContext).

I want to know where does the asynchronously part run, if there are no other threads created? If it runs on the same thread, shouldn't it block it due to long I/O request, or compiler is smart enough to move that action to another thread if it takes too long, and a new thread is used after all?

As I explain on my blog, truly asynchronous operations do not "run" anywhere. In this particular case (Task.Delay(1)), the asynchronous operation is based off a timer, not a thread blocked somewhere doing a Thread.Sleep. Most I/O is done the same way. HttpClient.GetAsync for example, is based off overlapped (asynchronous) I/O, not a thread blocked somewhere waiting for the HTTP download to complete.


Once you understand how await uses its context, walking through the original code is easier:

static void Main(string[] args)
{
Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
MainAsync(args).Wait(); // Note: This is the same as "var task = MainAsync(args); task.Wait();"
Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);

Console.ReadKey();
}

static async Task MainAsync(string[] args)
{
Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);
await thisIsAsync(); // Note: This is the same as "var task = thisIsAsync(); await task;"
}

private static async Task thisIsAsync()
{
Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1); // Note: This is the same as "var task = Task.Delay(1); await task;"
Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);
}
  1. The main thread starts executing Main and calls MainAsync.
  2. The main thread is executing MainAsync and calls thisIsAsync.
  3. The main thread is executing thisIsAsync and calls Task.Delay.
  4. Task.Delay does its thing - starting a timer and whatnot - and returns an incomplete task (note that Task.Delay(0) would return a completed task, which alters the behavior).
  5. The main thread returns to thisIsAsync and awaits the task returned from Task.Delay. Since the task is incomplete, it returns an incomplete task from thisIsAsync.
  6. The main thread returns to MainAsync and awaits the task returned from thisIsAsync. Since the task is incomplete, it returns an incomplete task from MainAsync.
  7. The main thread returns to Main and calls Wait on the task returned from MainAsync. This will block the main thread until MainAsync completes.
  8. When the timer set by Task.Delay goes off, thisIsAsync will continue executing. Since there is no SynchronizationContext or TaskScheduler captured by that await, it resumes executing on a thread pool thread.
  9. The thread pool thread reaches the end of thisIsAsync, which completes its task.
  10. MainAsync continues executing. Since there is no context captured by that await, it resumes executing on a thread pool thread (actually the same thread that was running thisIsAsync).
  11. The thread pool thread reaches the end of MainAsync, which completes its task.
  12. The main thread returns from its call to Wait and continues executing the Main method. The thread pool thread used to continue thisIsAsync and MainAsync is no longer needed and returns to the thread pool.

The important takeaway here is that the thread pool is used because there's no context. It is not automagically used "when necessary". If you were to run the same MainAsync/thisIsAsync code inside a GUI application, then you would see very different thread usage: UI threads have a SynchronizationContext that schedules continuations back onto the UI thread, so all the methods will resume on that same UI thread.

Await and different thread id for the same code

No, it shouldn't be 3 in the code 2. It's all about synchronization context.

There is no synchronization context in console application, so task continuations are executed on thread pool in code 1.

WPF and WinForms applications have synchronization context to route execution back to UI thread to modify controls for instance.

Try this this.Text = await DoWorkAsync().ConfigureAwait(false); in the 2nd example and your code won't be returned to thread 1.

Also check my answer here Is ConfigureAwait(false) required on all levels of the async chain when no real I/O call is involved?

What Is SynchronizationContext

Does the use of async/await create a new thread?

In short NO

From Asynchronous Programming with Async and Await : Threads

The async and await keywords don't cause additional threads to be
created. Async methods don't require multithreading because an async
method doesn't run on its own thread. The method runs on the current
synchronization context and uses time on the thread only when the
method is active. You can use Task.Run to move CPU-bound work to a
background thread, but a background thread doesn't help with a process
that's just waiting for results to become available.

I thought await continued on the same thread as the caller, but it seems not to

When you await, by default the await operator will capture the current "context" and use that to resume the async method.

This "context" is SynchronizationContext.Current unless it is null, in which case it is TaskScheduler.Current. (If there is no currently-running task, then TaskScheduler.Current is the same as TaskScheduler.Default, the thread pool task scheduler).

It's important to note that a SynchronizationContext or TaskScheduler does not necessarily imply a particular thread. A UI SynchronizationContext will schedule work to the UI thread; but the ASP.NET SynchronizationContext will not schedule work to a particular thread.

I suspect that the cause of your problem is that you are invoking the async code too early. When an application starts, it just has a plain old regular thread. That thread only becomes the UI thread when it does something like Application.Run.

Does await create another thread internally before shifting to the same thread as the caller (for UI application)

But my question is, does it create a new thread (internally) after IO is complete and before moving to the same UI thread.

Other threads may be temporarily used, but you don't need to worry about that.

In particular, I/O on .NET generally goes through an I/O completion port that is part of the thread pool. I/O threads are automatically added and removed as necessary. Fairly often, the I/O has some additional work to do before it actually is ready to return to your code (e.g., parsing HTTP response headers), so a lot of the BCL I/O code will actually use the I/O thread just to queue work to the thread pool. So a thread pool worker thread is often used (briefly) by I/O code.

Also, in this particular example, I believe there's a separate timer thread as well, that coalesces system timers. Naturally, this is an implementation detail and subject to change.

So, in summary, other threads may be created/destroyed/temporarily used, but you don't have to worry about it. They're all managed by the BCL or .NET runtime in a very efficient manner, striking a balance between reusing threads (minimizing churn) and minimizing resource use (especially memory).

Does .NET resume an await continuation on a new different thread pool thread or reuse the thread from a previous resumption?

Does .NET resume an await continuation on a new different thread pool thread or reuse the thread from a previous resumption?

Neither. By default, when awaiting Tasks, await will capture a "context" and use that to resume the asynchronous method. This "context" is SynchronizationContext.Current, unless it is null, in which case the context is TaskScheduler.Current. In your example code, the context is the thread pool context.

The other part of the puzzle is undocumented: await uses the TaskContinuationOptions.ExecuteSynchronously flag. This means that when the Task.Run task is completed (by thread 4), its continuations are run immediately and synchronously - if possible. In your example code, the continuation may run synchronously because there's enough stack on thread 4 and the continuation should be run on a thread pool thread and thread 4 is a thread pool thread.

Likewise, when AsyncThree completes, the continuation for AsyncTwo is run immediately and synchronously - again on thread 4 since it meets all the criteria.

This is an optimization that is especially helpful in scenarios like ASP.NET, where it's common to have a chain of async methods and have one task completing (e.g., a db read) that completes the entire chain and sends the response. In those cases you want to avoid unnecessary thread switches.

An interesting side effect of this is that you end up with an "inverted call stack" of sorts: the thread pool thread 4 ran your code and then completed AsyncThree and then AsyncTwo and then AsyncOne, and each of those completions are on the actual call stack. If you place a breakpoint on the WriteLine in AsyncOne (and look at external code), you can see where ThreadPoolWorkQueue.Dispatch (indirectly) called AsyncThree which (indirectly) called AsyncTwo which (indirectly) called AsyncOne.

Does a completed await continue its own async method on a new thread?

I have modified your code to show thread id.

class Program
{
static void Main(string[] args)
{
System.Threading.Thread.CurrentThread.Name = "ThisThread";
AnAsyncMethod();
while (true)
{
Console.WriteLine("External While loop. Current Thread: " + $"{System.Threading.Thread.CurrentThread.Name}- {System.Threading.Thread.CurrentThread.ManagedThreadId}");
System.Threading.Thread.Sleep(200);

}
}

public static async void AnAsyncMethod()
{
for (int i = 0; i < 10; i++)
{
FakeWork();
Console.WriteLine("doing fake pre-await work. Current Thread: " + $"{System.Threading.Thread.CurrentThread.Name}- {System.Threading.Thread.CurrentThread.ManagedThreadId}");
}
await System.Threading.Tasks.Task.Run(FakeWork);// Task.Run(FakeWork);
for (int i = 0; i < 10; i++)
{
FakeWork();
Console.WriteLine("doing fake post-await work. Current Thread: " + $"{System.Threading.Thread.CurrentThread.Name}- {System.Threading.Thread.CurrentThread.ManagedThreadId}");
}
Console.WriteLine("hello");
}

public static void FakeWork()
{
System.Threading.Thread.Sleep(100);
StackTrace stackTrace = new StackTrace();

Console.WriteLine($"Caller of Fakework method is {stackTrace.GetFrame(1).GetMethod().Name}. Current Thread: " + $"{System.Threading.Thread.CurrentThread.Name}- {System.Threading.Thread.CurrentThread.ManagedThreadId}");

}
}

and I get this output
output

Now you are getting post await in a separate thread. now this happens because

When it calls Task.Run, it queues the expensive CPU-bound operation, Fakework(), on the thread pool and receives a Task handle. Fakework() is eventually run concurrently on the next available thread

for more reference read async for cpu-bound method

https://learn.microsoft.com/en-us/dotnet/standard/async-in-depth

so async await job is to schedule task on the thread-pool and base on default scheduler algorithm it will use same thread or spin-off new thread

why the caller method and async method have the same thread id

One of the big points of async/await is if you have a SynchronizationContext, like the WPF's DispatcherSynchronizationContext, work started on that thread will continue on that thread after the await unless you tell it not to.

Console applications do not have a SynchronizationContext so it uses the default context which schedules threads on the thread pool, that is why you see different behavior from WPF and a console app.

To tell async/await it does not need to keep on the same sync context you can use .ConfigureAwait(false) when you await, it will then use the default thread pool context to do the callback if needed.

private async Task Test()
{
Log += "Command begin: " + DateTime.Now + "\r\n";
Log += "Command thread: " + Thread.CurrentThread.ManagedThreadId + "\r\n";
var getStringAsync = GetStringAsync();
Log += "Work in Command...\r\n";
Log += "Work in Command which not related to the result of async method will complete: " + DateTime.Now + "\r\n";
Log += "Work in Command which not related to the result of async method will complete, thread: " +
Thread.CurrentThread.ManagedThreadId + "\r\n";
string result = await getStringAsync.ConfigureAwait(false);
Log += "Command will complete: " + DateTime.Now + "\r\n";
Log += "Command will complete, thread: " + Thread.CurrentThread.ManagedThreadId + "\r\n";
Log += result + "\r\n";
}

private async Task<string> GetStringAsync()
{
Log += "Async method begin: " + DateTime.Now + "\r\n";
Log += "Async method thread: " + Thread.CurrentThread.ManagedThreadId + "\r\n";
Log += "Work in Async method... \r\n";
await Task.Delay(10000).ConfigureAwait(false);
Log += "Async method will complete: " + DateTime.Now + "\r\n";
Log += "Async method will complete, thread: " + Thread.CurrentThread.ManagedThreadId + "\r\n";
return "GetStringAsync method completed!";
}

Note, doing .ConfigureAwait(false) does not guarantee that the rest of the code will be on the thread pool, if the task is in the Completed state the code will execute synchronously and stay on whatever thread originally called await.

See the artice "It's All About the SynchronizationContext" for more info.



Related Topics



Leave a reply



Submit