0

I have some code where I need to read a web page (it's XML) and then process it. Until I get the XML there's nothing I can do with it.

The entire read & process I'm putting in a task. But within the task itself, is there any advantage to calling HttpWebRequest.GetResponseAsync() instead of HttpWebRequest.GetResponse()? I can't pass a cancellable token to GetResponseAsync() and so it seems to me in this case I should make the blocking call.

And this is based on my (still learning) understanding of await. As I understand it:

  1. Calling await on an async method means do not block on the method and return immediately returning a Task object.
  2. Calling await on a Task object means block until the task completes or is cancelled.

Update: Critical piece of info I forgot - this is code in a task. So I'm already not blocking the UI. The question is, in this background task, which is funcamentally a series of sequential actions, is there any value in using async calls?

David Thielen
  • 28,723
  • 34
  • 119
  • 193
  • 1
    `await` does not block, regardless of its target. Also, use `HttpClient` instead of `HttpWebRequest`. `HttpClient` does support `CacellationToken`. – Dai Jan 01 '20 at 13:12
  • 1
    The question focuses on the wrong detail. Whether or not to use async code depends on what *other* things are going on in the app. Where "other" is frequently a user interface. Sometimes a server-style app that needs to handle a lot of client service requests. If the delay you get from a slow web server doesn't matter then don't use async code. – Hans Passant Jan 01 '20 at 13:41
  • Hi, Dave. Hope things are going well. A blocking call blocks the thread while waiting for the result. `await` does not block the thread. This distinction can be significant if the thread is a UI thread (blocking the UI thread for a long time is a bad idea) or if you have lots of simultaneous operations (you may run out of threads). – Raymond Chen Jan 01 '20 at 13:54
  • Hi David. This is why async / await is hard. One needs to figure out the words first. You need to remember that blocking means holding the thread active in the process, that is how you need to mentally map the phrase "to block". Waiting and blocking is not the same. The `await GetResponseAsnyc()` waits but does not block where `GetResponse()` waits and blocks with clogging the process / OS mermory. The hole major purpose of async / await is memory management. The paralellization aspect of it is more of a by product as it is manifested through TPL / Task which you may or not have to await. – Vedran Mandić Apr 28 '21 at 14:06

2 Answers2

3

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:

  1. 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.

  2. 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.

Dai
  • 141,631
  • 28
  • 261
  • 374
  • This helps a LOT. But a follow up question. Reading https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/ it has the code "Task eggsTask = FryEggs(2); Egg eggs = await eggsTask;" - doesn't that second await block until the task completes? Or am I totally misunderstanding all this? – David Thielen Jan 01 '20 at 16:23
  • @DavidThielen It's important to note that `async`/`await` **only makes sense for non-CPU-bound operations** (namely IO, but there are other scenarios too). W.r.t. `await eggsTask`, it doesn't "block" - the thread yields itself back to the scheduler while the egg-frying operation (which represents an asynchronous IO operation) completes. It's because the thread is returned to the scheduler that allows the same thread to perform the window message-pump when otherwise it would be blocked (which causes UI freezes). – Dai Jan 01 '20 at 16:39
  • @DavidThielen In a single-threaded application with no other concurrent processing (such as web-server requests) or UI thread - such as a command-line utility program, then there is no need to yield the thread back to the scheduler - however it's still a good idea because the async operations being invoked may still want to use the scheduler for their own purposes (such as having IO timeouts, etc). Internally within OS kernels all IO is asynchronous anyway. – Dai Jan 01 '20 at 16:43
  • I think part of the problem in the example is it never shows the code for say ApplyButter(toast);. But somewhere there has to be code that says wait until these async tasks complete. Where/how is that done? thanks – David Thielen Jan 01 '20 at 17:18
  • @DavidThielen There isn't any code that says "wait until these tasks complete" *anywhere in the entire computer* because asynchronous operations are fundamentally different to normal blocking function calls. – Dai Jan 01 '20 at 17:21
  • @DavidThielen `ApplyButter` would be something like "send message to butter-applying robot to apply the butter, then have the robot press this button on this computer when it's done" - and that button press triggers a hardware interrupt which triggers the OS's async completion handlers, which then calls back into .NET Code. There is no "waiting". – Dai Jan 01 '20 at 17:23
-1

Yes, it is pointless. I cannot see why using async/await inside a single thread which you need to use the returning value will EVER make a difference. The chosen answer uses WinForms as an example, which can demonstrate blocking, but when processing a WEB request on the server, there is nothing ever to be "blocked", since subsequent requests get their own thread.

David
  • 1