Introduction
The TPL can be used to keep a desktop application responsive during long duration tasks. Those programmers
who have used the BackgroundWorker
to keep their application responsive and to provide async cancellation and to update
a progress bar from the background process will be able to see how Microsoft's latest incarnation of task scheduling and
management provides an even simpler and easier-to-use mechanism for using background tasks in WPF applications.
Background
Before the introduction of the TPL, creating and managing background tasks, especially in WinForm and then WPF applications,
required the programmer to handle the code for these tasks. The BackgroundWorker
class was able to provide a pre-defined
mechanism for providing background and UI task synchronization, but it involved understanding just how the
BackgroundWorker
provided these capabilities (using the async callback model). The BackgroundWorker
was an improvement over handling these
programming tasks in an ad-hoc manner, but now a newer, better model is available. All of this functionality is now provided by the new
TPL and the CancellationToken mechanism.
Using the code
The code in the simple application accompanying this article demonstrates how to add background processing and a progress bar
to a WPF application using the TPL. The application uses an inefficient recursive algorithm to calculate the nth value of the Fibonacci series, a perfect example of a long-running process. A value greater than 40
on the slider bar will take enough time to allow you to cancel the operation before it completes. A slider bar is used to set the the number of the element to calculate in the series. Two buttons are provided,
Start Async and Cancel Async, which are self-explanatory. The result of the calculation is displayed in the text box.
The StartAsync
button handler contains the code that starts the algorithm in a background process. It also creates the CancellationToken
that is passed to the background process to allow it to test for a
cancellation request. The actual algorithm is contained in the ComputeFibonacci
method. As you can see in the source code, not much needs to be changed to add the async capabilities to the code.
To cancel the operation, the CancelAsync
handler calls the
Cancel
method on the CancellationTokenSource
like this:
tokenSource.Cancel()
.
Cancellation
The rules for enabling cancellation are:
- Create a
CancellationTokenSource
in the thread that will raise the cancel request, in my example this is the UI thread
- Create a
CancellationToken
and pass it as a parameter to both the long running method (ComputeFibonacci
) and the Task running the method (fibTask
)
Let's look at some details of the code. We need to create a new instance of the
CancellationToken
each time we kick-off the process:
tokenSource = new CancellationTokenSource();
var ct = tokenSource.Token;
...
We can pass the cancellation token, ct, to the background processes so they can "cooperatively" process the cancel request.
Creating the background task
Next we need to kick-off the background process, but first we need to save the UI
context for synchronization purposes:
var UISyncContext = TaskScheduler.FromCurrentSynchronizationContext();
Task<long> fibTask = Task.Factory.StartNew(() => ComputeFibonacci(numberToCompute, ct, UISyncContext), ct);
...
Finally, we need to chain a task to display the results of the ComputeFibonacci
method, this is one of
the features of the TPL:
fibTask.ContinueWith((antecedent) =>
{
if (ct.IsCancellationRequested)
{
ManageDisplay("Canceled", false);
}
else
{
ManageDisplay(fibTask.Result.ToString(), false);
}
}, UISyncContext);
...
The ContinueWith
method chains the new task to the "antecedant" task so that it will automatically run
after the previous task completes. Also the
second task is automatically synchronized with the UI thread using the UISyncContext
that we retrieved earlier, which is passed as a parameter to the
ContinueWith
method. This is important because you can't
update the UI screen in any other thread but the UI thread. The result of the
computation is passed back to the UI thread in fibTask
's Result
property. We can check to see if the operation was canceled by using the cancellation token's
IsCancellationRequested
property.
Updating the Progress Bar
The rules for updating the progress bar are simple:
- Pass the UI thread to the long running method for synchronization
- Wrap the progress bar update in a Task that is synchronized with the UI thread
Let's see how that looks:
private long ComputeFibonacci(int n, CancellationToken token, TaskScheduler uiTask)
{
token.ThrowIfCancellationRequested();
...
if (percentComplete > highestPercentageReached)
{
highestPercentageReached = percentComplete;
Task.Factory.StartNew(() =>
{
progressBar1.Value = highestPercentageReached;
}, token, TaskCreationOptions.AttachedToParent, uiTask);
}
}
The first line of code in the ComputeFibonacci
method checks to see if a cancellation request has been sent by the user. This is all that is needed to handle the cancel request,
the code will not continue past this line
if the Cancel Async button has been pressed.
Next, this version
of the StartNew
method takes both a CancellationToken
(token) and the synchronization thread (uiTask
) as parameters. I don't use
the cancellation token, but if I needed to, it is available to this child task as well. The
TaskCreateOptions.AttachedToParent
parameter says that I want this task
to run as a child to the parent thread (ComputeFibonacci
). Basically this cancels the child thread automatically when the parent thread is canceled.
That's it! I now have an application that starts a long-running algorithm in the background, that displays progress to the UI and
that allows the user to cancel
the long-running process asynchronously. These simple steps can be repeated to
any level of complexity that is required to accomplish your programming needs.
Points of Interest
Some of you may be thinking, "What in the world is he doing creating and destroying a thread each time he wants to update the progress bar?" Well,
the beauty of the TPL is that these are "lightweight" threads that can be instantiated without much overhead.
There is very little performance penalty
associated with their creation, so you don't need to jump through a lot of hoops to use it. Just create it when you need
it, and throw it away when you're done.
History
- 12/2013 - Initial release.