Consider the following:
private async void btnSlowPoke_Click(object sender, EventArgs e)
{
await DoItAsync();
}
private async Task<int> SomeLongJobAsync()
{
for (int x = 0; x < 999999; x++)
{
//ponder my existence for one second
await Task.Delay(1000);
}
return 42;
}
public async Task<int> DoItAsync()
{
Console.Write("She'll be coming round the mountain");
Task<int> t = SomeLongJobAsync(); //<--On what thread does this execute?
Console.WriteLine(" when she comes.");
return await t;
}
- The first Write in
DoItAsync()
executes. SomeLongJobAsync()
starts.- The
WriteLine
inDoItAsync()
executes. DoItAsync()
pauses whileSomeLongJobAsync()
works away until it's done.SomeLongJobAsync()
completes, soDoItAsync()
returns.
Meanwhile, the UI is responsive.
On what thread does SomeLongJobAsync()
execute?
It is executed on the same thread. The documentation explains:
Short Answer
An
async
method fired by theGUI
thread will execute on the same thread, whenever there are CPU operation to execute. Otherasync
methods start running on the calling thread and continue on aThreadPool
thread.Long Answer
SomeLongJobAsync
starts executing on the calling thread (the one that printed "She'll be coming round the mountain") up until it reaches anawait
. Then a task is returned that represents the asynchronous operation + the continuation after it. When the entire operation is done the task will complete (unless it completes prematurely due to an exception or cancellation).When
Task.Delay(1000)
itself is "executing" there is no thread, because none is needed. And when finallyTask.Delay(1000)
ends, a thread is needed to resume on. Which thread it is depends on theSynchronizationContext
(by default there is none so the thread is aThreadPool
thread, but in aGUI
application it's the singe GUI thread, more here). That thread executes the rest of the code until it reaches another asynchronous point (i.e. anotherawait
) and so forth and so forth.SomeLongJobAsync
starts executing on the thread that called it, and if there is one, the currentSynchronizationContext
is saved in the state machine generated by theawait
mechanism.After the first
await
completes, the continuation of the method is posted on the currentSynchronizationContext
. In a GUI app this means that the continuation executes on the UI thread. In a console app, there is noSyncrhronizatonContext
so the continuation executes on a thread pool thread.You can check this by printing out the
ManagedThreadId
ofThread.CurrentThread
as your program executes. Consider this modified version of your code (which I ran from a console app on Linqpad):Output from Console App:
As you can see the method started runnung on thread 21, but as each of the await completed it continued on a thread pool thread and not always the same one. In this case 11, 12. If I run this in a Windows Forms app the output is this:
Output from Windows Forms App:
The important thing to realise is that async is not about creating threads, it's about replacing what used to be a blocking call by one that returns a continuation. A thread blocks when it is placed on a queue and it can then do nothing until the thing it blocked on becomes available (or a timeout or exception, etc). Blocking the UI thread is a terrible thing to do.
By contrast a continuation contains enough information captured at a point in the program for the thread to carry on ("continue") from that exact point at a later time. Obviously there is special stuff needed in all the Async calls for that to work, but that's what it does. It freezes the thread state into a continuation, parks that somewhere, returns immediately (so no blocking) and then (somehow) starts again from that state at a later time when the required information is available.
For that reason, you can assume that the work for both the async method and the "long job" will be done on the same thread, just not at the same time, and that the operating system will choose a good time to decide when to make those choices.
In practice there is a difference between threads that have a message pump (UI threads) and others, and there is the possibility of work being moved to a different thread, and there are various features in the
Task
, theSynchronizationContext
and thread pools to support more advanced scenarios.But I think the key to answering your question and for you to understand is this subtle use of something new called a continuation, and how it can capture the state of a program at one time for use later. Continuations have been used in functional languages for a long time, and are in some ways related to the concepts of futures and promises in other languages. Once you think in these terms you can forget about threads entirely.