20

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.

Synchronously Invoking a High-Latency Operation

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.

Listing 20.1: A Synchronous Web Request
using System;
using System.IO;
using System.Net;
 
public static class Program
{
    public const string DefaultUrl = "https://IntelliTect.com";
 
    public static void Main(string[] args)
    {
        if (args.Length == 0)
        {
            Console.WriteLine("ERROR: No findText argument specified.");
            return;
        }
        string findText = args[0];
 
        string url = DefaultUrl;
        if (args.Length > 1)
        {
            url = args[1];
            // Ignore additional parameters
        }
        Console.WriteLine(
            $"Searching for '{findText}' at URL '{url}'.");
 
        Console.WriteLine("Downloading...");
        using WebClient webClient = new();
        byte[] downloadData =
            webClient.DownloadData(url);
 
        Console.WriteLine("Searching...");
        int textOccurrenceCount = CountOccurrences(
            downloadData, findText);
 
        Console.WriteLine(
            @$"'{findText}' appears {
                textOccurrenceCount} times at URL '{url}'.");
    }
 
    private static int CountOccurrences(byte[] downloadData, string findText)
    {
        int textOccurrenceCount = 0;
 
        using MemoryStream stream = new(downloadData);
        using StreamReader reader = new(stream);
 
        int findIndex = 0;
        int length = 0;
        do
        {
            char[] data = new char[reader.BaseStream.Length];
            length = reader.Read(data);
            for (int i = 0; i < length; i++)
            {
                if (findText[findIndex] == data[i])
                {
                    findIndex++;
                    if (findIndex == findText.Length)
                    {
                        // Text was found
                        textOccurrenceCount++;
                        findIndex = 0;
                    }
                }
                else
                {
                    findIndex = 0;
                }
            }
        }
        while (length != 0);
 
        return textOccurrenceCount;
    }
}
Output 20.1
Searching for 'IntelliTect'...
http://www.IntelliTect.com
Downloading...
Searching...
'IntelliTect' appears 35 times at URL 'http://www.IntelliTect.com'.

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.

________________________________________

1. Introduced in C# 5.0.
{{ snackbarMessage }}