June 6th, 2012

Async in 4.5: Enabling Progress and Cancellation in Async APIs

The apps developers want to develop today are fast and fluid and the async features in .NET make this easier than ever. The world is also highly connected now which makes waiting for data a real concern for building great customer experiences. Alok Shriram from the .NET Base Class Library program management team continues his discussion on the async programming model by showing how to achieve both fast-and-fluid and connected scenarios. – Brandon


In this post, we will look at how you can enable your users to interact with async tasks, introduced in the Task Parallel Library (TPL), specifically by adding progress bars and cancellation buttons to your apps. It builds on an earlier post about the async programming model in the .NET Framework 4.5.

User experience and async

As a developer, you want to provide your users with great UI experiences in the apps that you build. You strive to produce visually compelling apps that are intuitive and that enable the completion of your users’ goals. It’s equally important that you provide feedback on progress and let users opt out of a lengthy operation. Progress bars and cancel buttons are the accepted and expected norm in apps today (and have been, for the last 20 years!).

The async programming model in the .NET Framework 4.5 provides an innovative new pattern for doing work in the background. The pattern is highly productive and solves common developer challenges. In many cases, you’ll want to choose async to ensure that your app is very responsive to users. For example, if you have a photo app, you might use the async pattern to load images from either a disk or a web service when you’re populating a photo gallery. While the images will likely load quickly, users understand that the pictures may load in an arbitrary order. As a result, exposing extra UI for monitoring or canceling the operation doesn’t make sense, and would only add noise to your app experience.

If a user-requested operation is expected to take a long time in your app, you’ll want to expose UI that enables the user to follow the progress and interact with the operation. Note that progress monitoring and cancellation aren’t new with the async pattern, but they’re important considerations when you adopt the async programming model. Just because you’re using the async pattern doesn’t mean that you won’t keep the user waiting! For example, your photo app may include a button that provides your users with the full list of EXIF tags used in their potentially very large photo collections. Since you cannot predict how long this operation will take, it makes sense to add UI for progress and cancellation. This is the scenario that we will discuss in this post.

sample_progress_dialog

Figure: Sample progress dialog box, with a cancel button

Enabling progress bars with async

We are all familiar with hourglasses and progress bars, and how they help users better understand what an app is doing. You’ll want to inform users that there will be a wait, provide a sense of how long the wait might be, and enable them to interact with the app during the wait. The progress and cancellation features of the async programming model enable you to deliver on all these needs.

At a high level, an app spawns off a task, and needs the task to report progress back to the app, enabling the app to present that information to the user. At a low level, the task has to understand the scope of work that it needs to accomplish, and then assess its progress on that work at relatively frequent intervals. For example, reporting progress twice while working on 100 items won’t enable a fluid user experience. For a better experience, you’d probably start with reporting progress once per item, and then change that as needed.

We introduced the IProgress<T> interface to enable you to create an experience for displaying progress. This interface exposes a Report(T) method, which the async task calls to report progress. You expose this interface in the signature of the async method, and the caller must provide an object that implements this interface. Together, the task and the caller create a very useful linkage (and could be running on different threads).

We also provided the Progress<T> class, which is an implementation of IProgress<T>. You are encouraged to use Progress<T> in your implementation, because it handles all the bookkeeping around saving and restoring the synchronization context. Progress<T> exposes both an event and an Action<T> callback, which are called when the task reports progress. This pattern enables you to write code that simply reacts to progress changes as they occur. Together, IProgress<T> and Progress<T> provide an easy way to pass progress information from a background task to the UI thread.

Async progress example

Let’s consider what it takes to build an async API that can report back progress. A simple use case for this would be an API that accepts a list of images, does some processing on them, and then uploads them to a web service. The signature of the method would look like this:

async Task<int> UploadPicturesAsync(List<Image> imageList, IProgress<int> progress)

Let’s implement this async method. We’ll focus specifically on the code that calculates and reports progress back to the UI thread:

async Task<int> UploadPicturesAsync(List<Image> imageList, IProgress<int> progress)
{
            int totalCount = imageList.Count;
            int processCount = await Task.Run<int>(() =>
            {
                int tempCount = 0;
                foreach (var image in imageList)
                {
                    //await the processing and uploading logic here
                    int processed = await UploadAndProcessAsync(image);
                    if (progress != null)
                    {
                        progress.Report((tempCount * 100 / totalCount));
                    }
                    tempCount++;
                }

                return tempCount;
            });
            return processCount;
}

Note that we used int as the generic T parameter in this example. int lends itself to a very simple implementation that covers many scenarios. Alternatively, you may find that you need to use one of your own types that encapsulates more information, such as both the progress and the entire scope of the work.

One of the biggest concerns you’ll have to address from the perspective of user experience is how frequently you report back progress. In this example, we are reporting back progress for every image that is processed and uploaded, which gives the user a sense that the operation is moving forward. However, if each iteration of this loop took a really long time (say, on the order of minutes), we would have to come up with a finer-grained way of reporting progress. On the other hand, if each iteration took milliseconds (think copying 1000 bytes from memory to disk), we might be able to make our progress reporting interval coarser-grained without sacrificing the responsive user experience.

On the consuming side of this API, on the UI thread, we first need to define an event handler Action<T>, which will be called when IProgress<T>.Report is invoked. The code for this is shown below.

 

void ReportProgress(int value)
{
//Update the UI to reflect the progress value that is passed back.
}

 

Next we need to set up our Progress<T> instance and invoke the async method, which is triggered by a button click event in this example:

    private async void Start_Button_Click(object sender, RoutedEventArgs e)
    {
    //construct Progress<T>, passing ReportProgress as the Action<T> 
        var progressIndicator = new Progress<int>(ReportProgress);
    //call async method
        int uploads=await UploadPicturesAsync(GenerateTestImages(), progressIndicator);
    }

Now we have an async task that reports progress back to the UI thread, enabling a good user experience. You can build on this code to create something more sophisticated, but the principles will remain the same.

Canceling async tasks

Cancellation is another aspect of user interaction that you need to consider to build compelling user experiences. The cancel button shown in the progress dialog box illustrated earlier is as iconic as the progress bar.

In the Task Parallel Library (TPL), cancellation is not strongly tied to a cancel button, although that is one of the ways in which it frequently manifests itself in a UI. Cancellation can also be used in situations where we want to time out long-running tasks or cancel parallel tasks.

Cancellation is controlled by the CancellationToken structure. You expose cancellation tokens in the signature of cancelable async methods, enabling them to be shared between the task and caller. In the most common case, cancellation follows this flow:

  1. The caller creates a CancellationTokenSource object.
  2. The caller calls a cancelable async API, and passes the CancellationToken from the CancellationTokenSource (CancellationTokenSource.Token).
  3. The caller requests cancellation using the CancellationTokenSource object (CancellationTokenSource.Cancel()).
  4. The task acknowledges the cancellation and cancels itself, typically using the CancellationToken.ThrowIfCancellationRequested method.

As part of supporting the task-based async programming model in the .NET Framework 4.5, we added the CancellationToken structure to the signatures of a large set of async APIs in the .NET Framework. For example, the HttpClient class exposes a GetAsync method overload that accepts a cancellation token. However, it is not essential for all async methods to support cancellation. For instance, if you look at the HttpContent class, the LoadIntoBufferAsync method does not expose an overload with a cancellation token.

You should consider using cancellation for all methods that may take a long time to complete. While evaluating the execution time of an API, you should consider the worst-case scenario to inform your decision.

Async cancellation example

Now that we’ve discussed how cancellation works, let’s write some code to consume a cancelable async API. Let’s extend our photo upload API by adding an overload that takes a cancellation token. The API signature now looks like the following:

async Task<int> UploadPicturesAsync(List<Image> imageList, 
                               IProgress<int> progress,
                               CancellationToken ct)

To consume this API, we write the following code in the UI button click event handler:

        private async void Start_Button_Click(object sender, RoutedEventArgs e)
        {
            var progressIndicator = new Progress<int>(ReportProgress);
            cts = new CancellationTokenSource();
            try
            {
                int x = await UploadPicturesAsync(GenerateTestImages(), progressIndicator, cts.Token);
            }
            catch (OperationCanceledException ex)
            {
                //Do stuff to handle cancellation
            }
        }

In order to cancel the UploadPicturesAsync task, we’ll add the following line of code to the cancel button click handler:

cts.Cancel();

This call will signal the token that was passed to UploadPicturesAsync that it has been canceled, and will request the task to be canceled. When consuming an async API that supports cancellation, you should ensure that you wrap the call to that method in a try/catch block, to catch any OperationCancelledException(OCE) exceptions that are thrown. If you don’t, an unhandled OCE could cause your app to fail.

If UploadPicturesAsync were a .NET Framework class library method, this is all you would need to worry about — the .NET Framework would take care of the mechanics of cancellation. However, if you’re implementing the UploadPicturesAsync method yourself, you would have to make sure that the cancellation token was handled appropriately. Let’s take a peek inside this method to see what is happening.

async Task<int> UploadPicturesAsync(List<Image> imageList, 
                               IProgress<int> progress,
                                CancellationToken ct)
        {
            int processCount = await Task.Run<int>(() =>
            {
                foreach (var image in imageList)
                {
             //await UploadAndProcessAsync (this is another method in the app)
               bool success = await UploadAndProcessAsync(image)
                    ct.ThrowIfCancellationRequested();
                    // progress logic here
                }
            },ct);
            return processCount;

As you can see, the mechanics of detecting and reacting to a cancellation token are quite simple. You do, however, need to ensure that your data is always left in a consistent state. For example, if the operation in our example is canceled halfway through an upload, we might have only some of the images uploaded. You would have to decide if that was OK.

Note: When you use the Task.Run with a delegate that supports cancellation, make sure to pass the cancellation token as a parameter to Task.Run, in addition to passing it to the lambda expression that you are constructing. Failure to do so will result in a cancellation operation on the lambda resulting in the task terminating in a faulted state.

As with progress polling, you’ll also need to decide how frequently you will check for the cancellation state. You should check frequently enough to ensure that the user experience is seamless, but make sure that the checking does not slow down the operation.

Conclusion

The task-based async model is a powerful new innovation in the .NET Framework 4.5, as we discussed in our earlier post. It enables you to build applications that are both responsive and scalable. However, your applications should continue to provide user experiences that align with common expectations. The async programming model enables you to deliver on those expectations by providing patterns for cancellation and progress monitoring. These patterns are easy to adopt and require minimal changes to existing code.

The Visual Studio Asynchronous Programming site on MSDN is a great resource for samples, whitepapers and talks on the new language features and support. MSDN also has a great topic discussing best practices for progress and cancellation UI in apps. You should also check out the Parallel Programming with .NET blog for a more in-depth discussion of parallel programming.

Author

3 comments

Discussion is closed. Login to edit/delete existing comments.

  • robertok! !

    According to proper usage async/await in implementation UploadPicturesAsync (see Stephen Cleary https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html)
    You should not use Task.Run in implementation e.g.

    <code>

    Read more
  • Frans

    I also love the code and examples. Funny how two readers comment some 7 years after, and then only a month apart. The code shows up fine in Firefox…  🙂

  • Peter Maximilian

    Love the exemplary souce code here. Edge&Chrome display it in mixed with html form 🙂