async/await with the Windows UI

One place where synchronization is especially important is in the UI context. With the Windows UI, for example, a message pump processes messages such as mouse click and move events. Furthermore, the UI is single-threaded, so that interaction with any UI components (e.g., a text box) must always occur from the single UI thread. One of the key advantages of the async/await pattern is that it leverages the synchronization context to ensure that continuation work—work that appears after the await statement—will always execute on the same synchronization task that invoked the await statement. This approach is of significant value because it eliminates the need to explicitly switch back to the UI thread to update a control.

To better appreciate this benefit, consider the example of a UI event for a button click in WPF, as shown in Listing 20.14.

Listing 20.14: Synchronous High-Latency Invocation in WPF
private void PingButton_Click(
   object sender, RoutedEventArgs e)
{
    StatusLabel.Content = "Pinging...";
    UpdateLayout();
    Ping ping = new Ping();
    PingReply pingReply =
        ping.Send("www.IntelliTect.com");
    StatusLabel.Text = pingReply.Status.ToString();
}

Given that StatusLabel is a WPF System.Windows.Controls.TextBlock control and we have updated the Content property twice within the PingButton_Click() event subscriber, it would be a reasonable assumption that first “Pinging…” would be displayed until Ping.Send() returned, and then the label would be updated with the status of the Send() reply. As those experienced with Windows UI frameworks well know, this is not, in fact, what happens. Rather, a message is posted to the Windows message pump to update the content with “Pinging…,” but because the UI thread is busy executing the PingButton_Click() method, the Windows message pump is not processed. By the time the UI thread is freed and can look at the Windows message pump, a second Text property update request has been queued and the only message that the user is able to observe is the final status.

To fix this problem using TAP, we change the code highlighted in Listing 20.15.

Listing 20.15: Synchronous High-Latency Invocation in WPF Using await
private async void PingButton_Click(
    object sender, RoutedEventArgs e)
{
    StatusLabel.Content = "Pinging...";
    UpdateLayout();
    Ping ping = new Ping();
    PingReply pingReply =
        await ping.SendPingAsync("www.IntelliTect.com");
    StatusLabel.Text = pingReply.Status.ToString();
}

This change offers two advantages. First, the asynchronous nature of the ping call frees up the caller thread to return to the Windows message pump caller’s synchronization context, and it processes the update to StatusLabel.Content so that “Pinging…” appears to the user. Second, when awaiting the completion of ping.SendTaskAsync(), it will always execute on the same synchronization context as the caller. Also, because the synchronization context is specifically appropriate for the Windows UI, it is single-threaded; thus, the return will always be to the same thread—the UI thread. In other words, rather than immediately executing the continuation task, the TPL consults the synchronization context, which instead posts a message regarding the continuation work to the message pump. Next, because the UI thread monitors the message pump, upon picking up the continuation work message, it invokes the code following the await call. (As a result, the invocation of the continuation code is on the same thread as the caller that processed the message pump.)

A key code readability feature is built into the TAP language pattern. Notice in Listing 20.15 that the call to return pingReply.Status appears to flow naturally after the await statement, providing a clear indication that it will execute immediately following the previous line. However, writing what really happens from scratch would be far less understandable for multiple reasons.

Beginner Topic
Reviewing await Operators

There is no limitation on the number of times that await can be placed into a single method. In fact, such statements are not limited to appearing one after another. Rather, await statements can be placed into loops and processed consecutively one after the other, thereby following a natural control flow that matches the way code appears. Consider the example in Listing 20.16.

Listing 20.16: Iterating over an await Operation
private async void PingButton_Click(
object sender, RoutedEventArgs e)
{
    List<string> urls = new List<string>()
        {
            "www.habitat-spokane.org",
            "www.partnersintl.org",
            "www.iassist.org",
            "www.fh.org",
            "www.worldVision.org"
        };
    IPStatus status;
 
    Func<string, Task<IPStatus>> func =
        async (localUrl) =>
        {
            Ping ping = new Ping();
            PingReply pingReply =
                await ping.SendPingAsync(localUrl);
            return pingReply.Status;
        };
 
    StatusLabel.Content = "Pinging...";
 
    foreach (string url in urls)
    {
        status = await func(url);
        StatusLabel.Text =
            $@"{ url }{ status.ToString() } ({
                Thread.CurrentThread.ManagedThreadId })";
    }
}

Regardless of whether the await statements occur within an iteration or as separate entries, they will execute serially, one after the other and in the same order they were invoked from the calling thread. The underlying implementation is to string them together in the semantic equivalent of Task.ContinueWith() except that all of the code between the await operators will execute in the caller’s synchronization context.

The need to support TAP from the UI is one of the key scenarios that led to TAP’s creation. A second scenario takes place on the server, when a request comes in from a client to query an entire table’s worth of data from the database. As querying the data could be time-consuming, a new thread should be created rather than consuming one from the limited number allocated to the thread pool. The problem with this approach is that the work to query from the database is executing entirely on another machine. There is no reason to block an entire thread, given that the thread is generally not active anyway.

To summarize, TAP was created to address these key problems:

There is a need to allow long-running activities to occur without blocking the UI thread.
Creating a new thread (or Task) for non–CPU-intensive work is relatively expensive when you consider that all the thread is doing is waiting for the activity to complete.
When the activity completes (either by using a new thread or via a callback), it is frequently necessary to make a thread synchronization context switch back to the original caller that initiated the activity.

TAP provides a new pattern that works for both CPU-intensive and non–CPU-intensive asynchronous invocations—one that all .NET languages support explicitly.

{{ snackbarMessage }}
;