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 HttpClient 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 static HttpClient HttpClient { getset; } = new();
    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...");
        byte[] downloadData =
            HttpClient.GetByteArrayAsync(url).Result;
 
        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(), 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.

________________________________________

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