Skip to content

Asynchronous programming

Oyvind Ohra edited this page Jan 1, 2022 · 7 revisions

Traditionally the only approach to avoid performance bottlenecks and enhance the overall responsiveness was to perform multiple operations at the same time. Operations with the potential of holding up other operations can execute on separate threads, a process known as multithreading or free threading.

Applications that use multithreading are more responsive to user input because the user interface stays active as processor-intensive tasks execute on separate threads. Multithreading is also useful when you create scalable applications, because you can add threads as the workload increases.

The figure below illustrates three tasks executed in parallel, completing much faster than in sequence, and more importantly allowing the UI-thread, e.g., Thread 1, to stay responsive (react to user interaction / visual updates).

Multithreading is, however, quite complicated to code, making them difficult to write, debug, and maintain.

C# 5 introduced a simplified approach, async programming, that leverages asynchronous support in .NET, and the Windows Runtime. The compiler does the difficult work that the developer used to do, and your application retains a logical structure that resembles synchronous code. As a result, you get all the advantages of asynchronous programming with a fraction of the effort.

Asynchrony proves especially valuable for applications that access the UI thread because all UI-related activity usually shares one thread. If any process is blocked in a synchronous application, all are blocked. Your application stops responding, and you might conclude that it has failed when instead it's just waiting.

The figure below illustrates how Task 1 yields for other tasks and then finishes.

C# has a language-level asynchronous programming model, which allows for easily writing asynchronous code. It follows what is known as the ask-based Asynchronous Pattern (TAP).

The asynchronous model

The task-based asynchronous pattern (TAP) is based on the Task and Task<TResult> types in the System.Threading.Tasks namespace, which are used to represent arbitrary asynchronous operations. If the result of the operation is awaited and only accessed once the ValueTask<TResult> may be used, however, be aware of the limitations.

The model is supported by the async and await keywords. The await keyword is where the magic happens. It yields control to the caller of the method that performed await, and it ultimately allows a UI to be responsive or a service to be elastic.

The model is fairly simple in most cases:

  • For I/O-bound code, you await an operation that returns a Task or Task<T> inside of an async method. See the I/O-bound example.
  • For CPU-bound code, you await an operation that is started on a background thread with the Task.Run method. See the CPU-bound example.

See the section on Recognize CPU-bound and I/O-bound work.

To dig deeper into the use of async, check out the Async in depth and Task asynchronous programming model articles.

  • async methods need to have an await keyword in their body or they will never yield!

  • Add "Async" as the suffix of every async method name you write.

  • async void should only be used for event handlers.

  • Tread carefully when using async lambdas in LINQ expressions.

  • Write code that awaits Tasks in a non-blocking manner.

  • Consider using ValueTask where possible.

  • Consider using ConfigureAwait(false), see the ConfigureAwait FAQ.

  • Write less stateful code. Don't depend on the state of global objects or the execution of certain methods. Instead, depend only on the return values of methods.

Samples

I/O-bound example

Create a Blank App, Packaged (WinUI 3 in Desktop)as described here.

Locate the MainWindow.xaml file and replace the StackPanel-element with the following:

<StackPanel>
    <Button x:Name="downloadButton">Download</Button>
    <Button x:Name="hitMeButton">Hit me!</Button>
    <TextBlock x:Name="output"></TextBlock>
</StackPanel>

Locate the MainWindow.xaml.cs file and replace the class definition with the following:

public sealed partial class MainWindow : Window
{
    private readonly HttpClient _httpClient = new();
    private const string UriString = "https://www.msn.com/";

    public MainWindow()
    {
        InitializeComponent();

        downloadButton.Click += async (o, e) =>
        {
            Output("Download started.");
            downloadButton.IsEnabled = !downloadButton.IsEnabled;

            string stringData = default;
            // Repeat 20 times just to make it appear slow 🙈
            for (int i = 0; i < 20; i++)
                // This line will yield control to the UI as the request from the web service is happening
                stringData += await _httpClient.GetStringAsync(UriString);

            Output($"Download completed. Downloaded bytes: {stringData.Length}");
            downloadButton.IsEnabled = !downloadButton.IsEnabled;
        };

        hitMeButton.Click += (o, e) =>
        {
            Output("Ouch, not so hard!");
        };
    }

    private void Output(string text)
    {
        output.Text += text + Environment.NewLine;
    }
}

Run the app, hit F5, and observe that the UI remains responsive even though the app is busy downloading the content.

CPU-bound example

Create a Console Application as described in the Create a project section. NB! Use .NET 5 as target framework rather than .NET Core 3.1 as the tutorial describes.

Replace the class definition with the following:

class Program
{
    static async Task Main()
    {
        Console.WriteLine("Calling async methods in sequence.");
        var sw = Stopwatch.StartNew();
        Console.WriteLine(await GreetingAsync("master"));
        Console.WriteLine(await GreetingAsync("student"));
        Console.WriteLine(await GreetingAsync("all"));
        Console.WriteLine($"Done after {sw.ElapsedMilliseconds / 1000} seconds.");

        Console.WriteLine("Calling async methods in parallel.");
        sw = Stopwatch.StartNew();
        Task<string> t1 = GreetingAsync("master");
        Task<string> t2 = GreetingAsync("student");
        Task<string> t3 = GreetingAsync("all");
        string[] results = await Task.WhenAll(t1, t2, t3);
        foreach (var result in results)
        {
            Console.WriteLine(result);
        }
        Console.WriteLine($"Done after {sw.ElapsedMilliseconds / 1000} seconds, exiting.");
    }

    private static async Task<string> GreetingAsync(string name)
    {
        // The work is started on a background thread with the Task.Run method.
        return await Task.Run(() =>
        {
            // We're sleeping for 3 seconds just to emulate some advanced work 🙈
            Thread.Sleep(3000);
            return $"Hello {name}";
        });
    }
}

Cancellation

In TAP, cancellation is optional for both asynchronous method implementers and asynchronous method consumers. If an operation allows cancellation, it exposes an overload of the asynchronous method that accepts a cancellation token (CancellationToken instance). Any code that is asynchronously waiting for a cancelled task through use of language features continues to run but receives an OperationCanceledException or an exception derived from it.

For more information, see the Cancellation and Choosing the overloads to provide sections.

Cancellation sample

Continuing on the I/O-bound example above, locate the MainWindow.xaml file and replace the StackPanel-element with the following:

<StackPanel>
    <Button x:Name="downloadButton">Download</Button>
    <Button x:Name="hitMeButton">Hit me!</Button>
    <Button x:Name="cancelButton" IsEnabled="False">Cancel download</Button>
    <TextBlock x:Name="output"></TextBlock>
</StackPanel>

Locate the MainWindow.xaml.cs file and replace the class definition with the following:

public sealed partial class MainWindow : Window
{
    private readonly HttpClient _httpClient = new();
    private const string UriString = "https://www.msn.com/";
    private CancellationTokenSource cts;

    public MainWindow()
    {
        InitializeComponent();

        downloadButton.Click += async (o, e) =>
        {
            Output("Download started.");
            downloadButton.IsEnabled = !downloadButton.IsEnabled;
            cancelButton.IsEnabled = !cancelButton.IsEnabled;

            string stringData = default;
            cts = new();
            try
            {
                // repeat 20 times just to make it appear slow 🙈
                for (int i = 0; i < 20; i++)
                    // This line will yield control to the UI as the request from the web service is happening
                    stringData += await _httpClient.GetStringAsync(UriString, cts.Token);

                Output($"Download completed. Downloaded bytes: {stringData.Length}");
            }
            catch (TaskCanceledException)
            {
                Output("Download cancelled!");
            }
            finally
            {
                downloadButton.IsEnabled = !downloadButton.IsEnabled;
                cancelButton.IsEnabled = !cancelButton.IsEnabled;
            }
        };

        hitMeButton.Click += (o, e) =>
        {
            Output("Ouch, not so hard!");
        };

        cancelButton.Click += (o, e) =>
        {
            Output("Cancelling in progress...");
            cts.Cancel();
        };
    }

    private void Output(string text)
    {
        output.Text += text + Environment.NewLine;
    }
}

Progress reporting

Some asynchronous operations benefit from providing progress notifications; these are typically used to update a user interface with information about the progress of the asynchronous operation.

For more information, see the Progress reporting and Choosing the overloads to provide sections.