I understand you're asking what the bother of await
is when asynchronous code performs step sequentially (i.e. when a function could be both async and non-async function).
In your example, using HttpWebRequest
(which is moot as we should all be using HttpClient
, but I digress):
Traditional, blocking code - assuming this is a WinForms context:
// (Error handling omitted for brevity)
void PopulateTextBox() {
using( HttpWebRequest req = HttpWebRequest.CreateHttp( "https://www.bing.com" ) )
using( HttpWebResponse res = (HttpWebResponse)req.GetResponse() )
using( Stream body = res.GetResponseStream() )
using( StreamReader rdr = new StreamReader( body ) )
{
this.textbox.Text = rdr.ReadToEnd();
}
}
And async code equivalent:
// Note that HttpWebResponse does not have a `GetResponseStreamAsync()` method.
// (Error-handling and ConfigureAwait omitted for brevity)
async Task PopulateTextBoxAsync() {
using( HttpWebRequest req = HttpWebRequest.CreateHttp( "https://www.bing.com" ) )
using( HttpWebResponse res = (HttpWebResponse)( await req.GetResponseAsync() ) )
using( Stream body = res.GetResponseStream() ) // HttpWebResponse does not have a `GetResponseStreamAsync()` method
using( StreamReader rdr = new StreamReader( body ) )
{
this.textbox.Text = await rdr.ReadToEndAsync();
}
}
Given that all the different program steps happen sequentially it's easy to miss the advantages of asynchronous code - however:
await
does not block, the thread yields back to the scheduler (just like in Windows 3.xx days) - allowing the GUI thread to pump window-messages, allowing the the program to handle UX events that you may still want to allow and to allow window redrawing (e.g. if the user resizes the window while it's running this method). Synchronous code just doesn't allow this and users have a poor experience from a temporarily frozen window - if you've ever used Outlook or SQL Server Management Studio on a low-speed or high-latency connection you know what I'm talking about, and it reduces the user's confidence in the quality of your product.
You can argue w.r.t freezing windows "well, that's what BackgroundWorker
(or background threads in general) are for!" and yes - that's why they were added in 2005, but the problem is they don't scale . If you have anything beyond a trivial application you can't start a new background thread for every new non-UI-freezing task you want to accomplish because each thread in Windows adds over a megabyte to your process' private memory usage (due to the default stack size). By using asynchronous code that runs on the same thread (or an extant thread in the thread-pool) you won't cause your program's memory to go up. Also, instantiating threads on Windows isn't particularly cheap either.
- This is why all the cool web-servers and web-application platforms today (like Nginx, Node.JS, ASP.NET Core, etc) are big on async IO because it means threads aren't blocked - it massively increases the simultaneous number of connections a webserver can handle. The idea of having "one thread per connection" that started in the 1990s (ironically, to simplify socket programming) only ended up complicating it when it ran into scaling issues - because then a late-1990s machine with 256MB of RAM just couldn't handle 1,000 simultaneous connections without massive page thrashing, ew!)
- Interesting note: For web-applications in IIS, the default thread size is 256KB.
So going back to the HttpWebRequest
example, with the above in mind, the following advantages become apparent:
Compared to not using any "background" features (like BackgroundWorker
, or background threads, or async/await
):
- The user can still interact with your program's GUI (even if you disable controls to prevent state-breaking events from firing) for things like window resizing and repainting the window.
- Windows won't tell the user that your program has "stopped responding".
Compared to using BackgroundWorker
or background threads in particular:
- You don't have to deal with the setup of each
BackgroundWorker
and juggling results (which appear in a separate event-handler method instead of the initiator).
- Or deal with mutable class state because
BackgroundWorker
really makes you use mutable Form
/UserControl
state and as I've gained experience in SWE work over the years I've learned to abhor mutable state (and mutable state makes working with concurrent reentrant code very difficult).
- Reduced memory use due to no new threads being created.
- Improved scale: you could have thousands of requests fired-off and your program will be fine - but if you fire-off a thousand
HttpWebRequest
instances using background threads you'll run into issues.
And finally, HttpClient
(which we should all be using instead of HttpWebRequest
) only provides async methods - so you have to use await
(or ContinueWith
, or an IAsyncResult
-adapter) to use it.