Reputation: 1953
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);
}
}
Upvotes: 4
Views: 1857
Reputation: 30502
What helped me a lot to understand the way async-await works was this restaurant metaphor by Eric Lippert. Search somewhere in the middle of the interview for async await.
Async await is only meaningful if your thread sometimes has to wait for something lengthy to complete, like writing a file to disk, querying data from a database, getting information from the internet. While waiting for these actions to complete, your thread is free to do other things.
Without using async-await, doing other things and continuing the original code after the lengthy processing would be cumbersome and difficult to understand and maintain.
That is when async-await comes to the rescue. Using async-await your thread doesn't wait until the lengthy process is completed. In fact it remembers that something still has to be done after the length processing in a Task object, and starts doing something else, until it needs the result of the lengthy process.
In Eric Lippert's metaphor: after starting to toast the bread, the cook doesn't wait until the thread is started. Instead he starts cooking eggs.
In code this would look like:
private async Task MyFunction(...)
{
// start reading some text
var readTextTask = myTextReader.ReadAsync(...)
// don't wait until the text is read, I can do other things:
DoSomethingElse();
// now I need the result of the reading, so await for it:
int nrOfBytesRead = await readTextTask;
// use the read bytes
....
}
What happens is that your thread enters the ReadAsync function. Because the function is async, we know there is somewhere an await in it. In fact, your compiler will warn you if you write an async function without an await. Your thread performs all code inside the ReadAsync until it reaches the await. Instead of really waiting your thread goes up in its call stack to see if it can do something else. In the example above, it starts DoSomethingElse().
After a while your thread sees the await readTextTask. Again, instead of really waiting it goes up its stack to see if there is some code that is not awaiting.
It continues doing this until everyone is awaiting. Then and only then your thread really can't do anything anymore and it starts waiting until the await in ReadAsync if finished.
This method has the advantage that your thread will be less waiting, thus your process will be finished earlier. Besides it will keep your callers (inclusive the UI) responsive, without having the overhead and difficulties of multiple threads.
Your code will look sequential, in fact it is not performed sequential. Every time an await is met, some code up in the call stack that is not awaiting will be executed. Note that although it is not sequential, it is still all done by one thread.
Note this all is still single threaded. A thread can only do one thing at a time, so while your thread is busy doing some heavy calculations, your callers can't do anything else, and your program still won't be responsive until your thread finishes doing the calculations. Async-Await won't help you with thead
That's why you see that time consuming procedure are started in a separate thread as an awaitable Task using Task.Run. This will free your thread to do other things. Of course this method is only meaningful if your thread really has something else to do while waiting for the calculations to finish and if the overhead of starting a new thread is less costly than doing the calculations yourself.
private async Task<string> ProcessFileAsync()
{
var calculationTask = Task.Run( () => HeavyCalcuations(...));
var downloadTask = downloadAsync(...);
// await until both are finished:
await Task.WhenAll(new Task[] {calculationTask, downloadTak});
double calculationResult = calculationTask.Result;
string downloadedText = downloadTask.Result;
return downloadedText + calculationResult.ToString();
}
Now back to your question.
Somewhere in the first ProcessUrlAsync is an await. Instead of doing nothing, your thread returns control to your procedure and remembers it still has some processing to do in the Task object downLoad1. It starts calling ProcessUrlAsync again. Does not wait for the result and starts a third download. Each time remembering that it still has something to do in Task objects downLoad2 and downLoad3.
Now your process really has nothing to do anymore, so it awaits for the first downLoad to complete.
This doesn't mean that your thread really is doing nothing, it goes up it's call stack to see if any of the callers is not awaiting and starts processing. In your example, the Start_Button_Click is awaiting, so it goes to the caller, which probably is the UI. The UI probably is not awaiting, so it is free to do something else.
After all downloads are completed your thread continues with displaying the results.
By the way, instead of awaiting three times, you can await for all tasks to finish using Task.WhenAll
await Task.WhenAll(new Task[] {downLoad1, download2, download3});
Another document that helped me a lot understanding async-await is Async And Await by the ever so helpful Stephen Cleary
Upvotes: 0
Reputation: 127603
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.
Upvotes: 8