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 WebClient 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() invokes CountOccurrences(), instantiates a WebClient, and invokes the synchronous method DownloadData() to download the content. 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 DownloadData() method rather than the simpler DownloadString() 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 the corresponding operation, 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.