Programming the Task-Based Asynchronous Pattern
As we saw in Chapter 19, tasks provide an abstraction for the manipulation of asynchronous work. Tasks are automatically scheduled to the right number of threads, and large tasks can be composed by chaining together small tasks, just as large programs can be composed from multiple small methods.
However, there are some drawbacks to tasks. The principal difficulty with tasks is that they turn your program logic “inside out.” To illustrate this, we begin the chapter with a synchronous method that is blocked on an I/O-bound, high-latency operation—a web request. We then revise this method by leveraging the async/await contextual keywords,1 demonstrating a significant simplification in authoring and readability of asynchronous code.
We finish the chapter with a look at asynchronous streams—a C# 8.0–introduced feature for defining and leveraging asynchronous iterators.
In Listing 20.1, the code uses a HttpClient to download a web page and search for the number of times some text appears. Output 20.1 shows the results.
The logic in Listing 20.1 is relatively straightforward—using common C# idioms. After determining the url and findText values, Main(), using an existing HttpClient instance, it invokes the asynchronous method GetByteArrayAsync() to download the content. It calls .Result to force the asynchronous method to execute synchronously and return the result. This approach to making asynchronous methods run synchronously is not recommend as it can cause deadlocks. Given the downloaded data, it passes this data to CountOccurrences(), which loads it into a MemoryStream and leverages a StreamReader’s Read() method to retrieve a block of data and search it for the findText value. (We use the GetByteArrayAsync() method rather than the simpler GetStringAsync() method so that we can demonstrate an additional asynchronous invocation when reading from a stream in Listing 20.2 and Listing 20.3.)
The problem with this approach is, of course, that the calling thread is blocked until the I/O operation completes; this is wasting a thread that could be doing useful work while the operation executes. For this reason, we cannot, for example, execute any other code, such as code that asynchronously indicates progress. In other words, “Download…” and “Searching…” are invoked before their corresponding operations, not during the operation. While doing so would be irrelevant here, imagine that we wanted to concurrently execute additional work or, at a minimum, provide an animated busy indicator.