Peregrine's View

Yet another C# / WPF / MVVM blog …

C# General

C# – Async / Await (Part 1)

There is nothing so fatal to character as half finished tasks.
David Lloyd George

For the next series of posts I’m going to look at one of the newer features of the C# language – async / await. I’ll start by busting a couple of myths commonly associated with this coding pattern.

  • Tasks are not the same as threads.
    Invoking an asynchronous method does not automatically mean that a new thread will be created. Many I/O methods use low level I/O request packets and interrupts to manage an operation, without requiring a new thread. For more on this see Stephen Cleary’s blog post. However, an asynchronous method may return on a different thread (from the threadpool, not a thread that you have to explicily create yourself) to the one it was called on. This is especially important for a platform like WPF where all updates to controls or other classes such as ObservableCollection must be done on the creating (UI) thread. A utility such as perDispatcherHelper that I’ve discussed in previous posts will help with this.
  • Using asynchronous methods will not automatically improve application performance.
    Making a method asynchronous will not make it run any faster. For example, if you’re loading a file from disk or downloading data from a web URL, synchronous and asynchronous methods will take the same amount of time. The key reason for using asynchronous methods is to avoid blocking the UI thread, creating a better experience for the user of your application. However, if you have multiple asynchronous operations to perform, there will be some benefit if you can run them in parallel – I’ll cover this in more detail in a future post.

The demo project for this post shows the basic async / await operations and introduces a useful helper class for task management. The core usage of async / await is a block of work that you want to perform without blocking the UI thread. In this case, DemoWorker.DoWork(), which will count for the specified number of steps, pausing between each one, and providing ongoing feedback to the caller. As a test of the helper class, DoWork() will throw an exception when it reaches step 8 (this is only applicable to the Task1 instance).

public static class DemoWorker
{
    // Simulate a long running task
    // Repeat tickCount times, pausing by tickInterval each loop, and providing feedback to the caller via progress
    public static async Task<string> DoWork(string workerId, int tickCount, TimeSpan tickInterval, IProgress<int> progress, CancellationToken token)
    {
        var i = 1;

        while (i <= tickCount && !token.IsCancellationRequested)
        {
            await Task.Delay(tickInterval, token).ConfigureAwait(false);

            token.ThrowIfCancellationRequested();

            progress.Report(i++);

            // test what happens when an exception is thrown inside the task
            if (!token.IsCancellationRequested && i > 8)
                throw new ApplicationException("Worker " + workerId + ": Bang!!!");
        }

        return token.IsCancellationRequested
            ? string.Empty
            : "Result from worker " + workerId;
    }
}

Some key features …

  • The pause is generated using Task.Delay() – you should never use Thread.Sleep() inside an asynchronous method.
  • In library classes, every time you use await, you should flag the task with .ConfigureAwait(false). This prevents capturing and blocking the UI context while task is executing.
  • Feedback to the caller is handled using an IProgress<T> implementation. The .net framework will automatically marshall all progress calls back to the UI thread.
  • CancellationToken provides a thread safe way for the caller to cancel a running task.

MainViewModel generates three instances of DemoWorker.DoWork() with different parameters, and runs them in parallel using Task.WhenAll(). You should avoid using Task.WaitAll() and other similar blocking methods that can’t be awaited. Each of these tasks has its own IProgress<int> implementation that will update the appropriate properties and bound UI controls. Each task has a continuation that will update the UI immediately the if task is completed ok, without waiting for the Task.WhenAll() call to complete.

perTaskHelper contains a number of methods to manage a task’s operation – handling timeout / cancellation / exceptions as required, and returning a status value. They are named similarly to the standard C# delegate classes – Actions just perform an operation and return the status, Functions perform an operation that return a data value along with the status. Any exception thrown within the task will be captured, providing a single returned status whatever the result of the task.

Note how the same CancellationTokenSource or its Token are used for the three tasks as well as the call to Task.WhenAll(...).ExecuteActionWithTimeoutAsync(...). This allows the individual DemoWorker.DoWork() tasks to be cancelled if the overall operation is cancelled or times out.

Run the demo application and note the behavior for various time out settings and also when the cancel button is clicked. The GlobalStatus string might look a little strange, showing “Completed OK” even when Task1 throws an exception within its DemoWorker.DoWork() call. However, this is just showing the response from the Task.WhenAll() call, which will always be Ok if it’s not otherwised cancelled or timed out, regardless of the status of the individual tasks.

Note how the three tasks are awaited again after Task.WhenAll() completes. This is considered best practice – there are some obscure cases where using Task.Result can block, even on a completed task.

In the next post, I’ll look at further uses for the async / await pattern within a C# / WPF application.

As usual the code samples for this blog are available on Github, along with my own personal C# / WPF library.

If you find this article useful, or you have any other feedback, please leave a comment below.

Leave a Reply

Your email address will not be published. Required fields are marked *