Microsoft says: “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.”
Here is the web request example Microsoft uses for explaining the use of async and await. (https://msdn.microsoft.com/en-us/library/mt674880.aspx). I pasted the relevant part of the sample code at the end of the question.
My question is, after each “var byteArray = await client.GetByteArrayAsync(url);”statement, control returns to CreateMultipleTasksAsync method, then invoking another ProcessURLAsync method. And after three downloads are invoked, then it starts to wait on the completion of the first ProcessURLAsync method to finish. But how can it proceed to the DisplayResults method if ProcessURLAsync is not running in a seperate thread? Because if it is not on a different thread, after returning control to CreateMultipleTasksAsync, it can never complete. Can you provide a simple control flow so that I can understand?
Let's assume the first client.GetByteArrayAsync method finished before Task download3 = ProcessURLAsync(..), when exactly is the first DisplayResults called?
private async void startButton_Click(object sender, RoutedEventArgs e)
{
resultsTextBox.Clear();
await CreateMultipleTasksAsync();
resultsTextBox.Text += "\r\n\r\nControl returned to startButton_Click.\r\n";
}
private async Task CreateMultipleTasksAsync()
{
// Declare an HttpClient object, and increase the buffer size. The
// default buffer size is 65,536.
HttpClient client =
new HttpClient() { MaxResponseContentBufferSize = 1000000 };
// Create and start the tasks. As each task finishes, DisplayResults
// displays its length.
Task<int> download1 =
ProcessURLAsync("http://msdn.microsoft.com", client);
Task<int> download2 =
ProcessURLAsync("http://msdn.microsoft.com/en-us/library/hh156528(VS.110).aspx", client);
Task<int> download3 =
ProcessURLAsync("http://msdn.microsoft.com/en-us/library/67w7t67f.aspx", client);
// Await each task.
int length1 = await download1;
int length2 = await download2;
int length3 = await download3;
int total = length1 + length2 + length3;
// Display the total count for the downloaded websites.
resultsTextBox.Text +=
string.Format("\r\n\r\nTotal bytes returned: {0}\r\n", total);
}
async Task<int> ProcessURLAsync(string url, HttpClient client)
{
var byteArray = await client.GetByteArrayAsync(url);
DisplayResults(url, byteArray);
return byteArray.Length;
}
private void DisplayResults(string url, byte[] content)
{
// Display the length of each website. The string format
// is designed to be used with a monospaced font, such as
// Lucida Console or Global Monospace.
var bytes = content.Length;
// Strip off the "http://".
var displayURL = url.Replace("http://", "");
resultsTextBox.Text += string.Format("\n{0,-58} {1,8}", displayURL, bytes);
}
}
The way it calls the function without creating a new thread is the main "UI" thread is constantly going through a queue of work to do and processing items in the queue one after another. A common term you might hear for this is the "Message Pump".
When you do a await
and you are running from the UI thread, once the call completes to GetByteArrayAsync
a new job will be put on the queue and when it becomes that job's turn it will continue with the rest of the code of the method.
GetByteArrayAsync
does not use a thread to do it's work either, it asks the OS to do the work and load the data in to a buffer and then it waits for the OS to tell it that the OS has finished loading the buffer. When that message comes in from the OS a new item goes in to that queue I was talking about earlier (kinda, i get in to that later), once it becomes that item's turn it will copy the small buffer it got from the OS to a bigger internal buffer and repeats the process. Once it gets all bytes of the file it will signal it is done to your code causing your code to put it's continuation on to the queue (the stuff I explained last paragraph).
The reason I said "kinda" when talking about GetByteArrayAsync
putting items in to the queue is there is actually more than one queue in your program. There is one for the UI, one for the "thread pool", and one for "I/O Completion ports" (IOCP). The thread pool and IOCP ones will generate or reuse short lived threads in the pool, so this technicaly could be called creating a thread, but a available thread was sitting idle in the pool no thread would be created.
Your code as-is will use the "UI queue", the code GetByteArrayAsync
is most likely using the thread pool queue to do it's work, the message the OS uses to tell GetByteArrayAsync
that data is available in the buffer uses the IOCP queue.
You can change your code to switch from using the UI queue to the thread pool queue by adding .ConfigureAwait(false)
on the line you perform the await.
var byteArray = await client.GetByteArrayAsync(url).ConfigureAwait(false);
This setting tells the await
"Instead of trying to use SynchronizationContext.Current
to queue up the work (The UI queue if you are on the UI thread) use the "default" SynchronizationContext
(which is the thread pool queue)
Let's assume the first "client.GetByteArrayAsync" method finished before "Task download3 = ProcessURLAsync(..)" then, will it be "Task download3 = ProcessURLAsync(..)" or "DisplayResults" that will be invoked? Because as far as I understand, they will both be in the queue you mention.
I will try to make a explicit sequence of events of everything that happens from mouse click to completion
WM_LBUTTONDOWN
message in the UI message queue.Button
control named startButton
receives the message the message, sees that the mouse was positioned over itself when the event was fired and calls its click event handlerstartButton_Click
startButton_Click
calls CreateMultipleTasksAsync
CreateMultipleTasksAsync
calls ProcessURLAsync
ProcessURLAsync
calls client.GetByteArrayAsync(url)
GetByteArrayAsync
eventually internally does a base.SendAsync(request, linkedCts.Token),
SendAsync
does a bunch of stuff internally that eventually leads it to send a request from the OS to download a file from native DLLs.So far, nothing "async" has happened, this is just all normal synchronous code. everything up to this point behaves exactly the same if it was sync or async.
SendAsync
returns a Task
that is currently in the "Running" state.response = await sendTask.ConfigureAwait(false);
await
checks the status of the task, sees that it is still running and causes the function to return with a new Task in the "Running" state, it also asks the task to run some additional code once it finishes, but use the thread pool to do that additional code (because it used .ConfigureAwait(false)
).GetByteArrayAsync
returns a Task<byte[]>
that is in the "Running".await
sees that the returned Task<byte[]>
is in the "Running" state and causes the function to return with a new Task<int>
in the "Running" state, it also asks the Task<byte[]>
to run some additional code using SynchronizationContext.Current
(because you did not specify .ConfigureAwait(false)
), this will cause the additional code when ran to be put in to the queue we last saw in step 3.ProcessURLAsync
returns a Task<int>
that is in the "Running" state and that task is stored in to the variable download1
.download2
and download3
NOTE: We are still on the UI thread and have yet to yield control back to the message pump during this entire process.
await download1
it sees that the task is in the "Running" state and it asks the task to run some additional code using SynchronizationContext.Current
it then creates a new Task
that is in the "Running" state and returns it.await
the result from CreateMultipleTasksAsync
it sees that the task is in the "Running" state and it asks the task to run some additional code using SynchronizationContext.Current
. Because the function is async void
it just returns control back to the message pump.Ok, got all that? Now we move on to what happens when "work gets done"
Once you do step 10 at any time the OS may send a message using IOCP to tell the code it has finished filing a buffer, that IOCP thread may copy the data or it mask ask a thread pool thread to do it (I did not look deep enough to see which).
This process keeps repeating till all the data is downloaded, once it is fully downloaded the "extra code" (a delegate) step 12 asked the task to do gets sent to SynchronizationContext.Post
, because it used the the default context that delegate will get executed by the thread pool. At the end of that delegate it signals the original Task
that was returned that had the "Running" state to the completed state.
Once the Task<byte[]>
returned in step 13, awaited in step 14 it does it's SynchronizationContext.Post
, this delegate will contain code similar to
Delegate someDelegate () =>
{
DisplayResults(url, byteArray);
SetResultOfProcessURLAsyncTask(byteArray.Length);
}
Because the context you passed in was the UI context this delegate gets put in the queue of messages to be processed by the UI, the UI thread will get to it when it gets a chance.
Once ProcessURLAsync
for download1
completes that will cause the a delegate that looks kinda like
Delegate someDelegate () =>
{
int length2 = await download2;
}
Because the context you passed in was the UI context this delegate gets put in the queue of messages to be processed by the UI, the UI thread will get to it when it gets a chance. Once that one is done it does queues up a delegate that looks kinda like
Delegate someDelegate () =>
{
int length3 = await download3;
}
Because the context you passed in was the UI context this delegate gets put in the queue of messages to be processed by the UI, the UI thread will get to it when it gets a chance. Once that is done it queues up a delegate that looks kinda like
Delegate someDelegate () =>
{
int total = length1 + length2 + length3;
// Display the total count for the downloaded websites.
resultsTextBox.Text +=
string.Format("\r\n\r\nTotal bytes returned: {0}\r\n", total);
SetTaskForCreateMultipleTasksAsyncDone();
}
Because the context you passed in was the UI context this delegate gets put in the queue of messages to be processed by the UI, the UI thread will get to it when it gets a chance. Once the "SetTaskForCreateMultipleTasksAsyncDone" gets called it queues up a delegate that looks like
Delegate someDelegate () =>
{
resultsTextBox.Text += "\r\n\r\nControl returned to startButton_Click.\r\n";
}
And your work is finally complete.
I have made some major simplifactions, and did a few white lies to make it a little easier to understand, but this is the basic jist of what happens. When a Task
finishes it's work it will use the thread it was already working on to do the SynchronizationContext.Post
, that post will put it in to whatever queue the context is for and will get processed by the "pump" that handles the queue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With