I have been trying to convert some of my old asynchronous code to the .NET 4.0 Task API. In particular, I have been trying to convert some code that reported progress through the BackgroundWorker
class. Unfortunately, I haven’t seen a built in way to report the progress of a Task
, so I decided to take a go at doing this.
My first thought was to just pass a SynchronizationContext
into the delegate used to create the Task
. I didn’t like that solution because now the idea of the Task
as a piece of work is polluted with code simply to make a callback onto the correct thread. Ideally I wouldn’t have to worry about that every time I wanted to report progress from a Task
.
The solution I eventually came up with was to use subtasks. When a new Task
is created while another Task
is being executed, it is added as a child of that Task
. These Task
s are aggregated by the parent Task
, and can share the same CancellationToken
so that if the main Task
is cancelled, the child Task
s will also have a cancellation requested. By waiting for the children to complete, the main Task
won’t complete successfully unless all the sub tasks have.
In order to make the programming model as similar as possible to standard Task
s, I created a Create
method in the ReportableTask
static class:
public static class ReportableTask
{
public static Task Create<TResult>(Action<Task<TResult>> reportProgress,
params Func<TResult>[] createWorkTasks)
{
if (createWorkTasks == null || createWorkTasks.Length == 0)
throw new ArgumentNullException("createWorkTasks",
"Must specify at least one function to create a reportable task with.");
TaskScheduler current =
TaskScheduler.FromCurrentSynchronizationContext();
return new Task(() =>
{
List<Task> children = new List<Task>();
foreach (var func in createWorkTasks)
{
Task<TResult> task = new Task<TResult>(o =>
{
if (AcknowledgePendingCancellations())
return default(TResult);
TResult res = ((Func<TResult>)o)();
if (AcknowledgePendingCancellations())
return default(TResult);
return res;
}, func, TaskCreationOptions.RespectParentCancellation);
task.ContinueWith(reportProgress,
TaskContinuationOptions.OnlyOnRanToCompletion, current);
task.Start();
children.Add(task);
}
if (!AcknowledgePendingCancellations())
{
try
{
Task.WaitAll(children.ToArray(),
Task.Current.CancellationToken);
}
catch (OperationCanceledException)
{
AcknowledgePendingCancellations();
}
}
});
}
}
I also created StartNew
methods to simplify common Task
creation scenarios:
public static Task StartNew<TResult>(Action<Task<TResult>> reportProgress,
params Func<TResult>[] createWorkTasks)
{
Task t = Create<TResult>(reportProgress, createWorkTasks);
t.Start();
return t;
}
public static Task StartNew<TResult>(Action<Task<TResult>> reportProgress,
Action<Task> continueWith,
TaskContinuationOptions continueOptions,
params Func<TResult>[] createWorkTasks)
{
Task t = Create<TResult>(reportProgress, createWorkTasks);
t.ContinueWith(continueWith,
continueOptions,
TaskScheduler.FromCurrentSynchronizationContext());
t.Start();
return t;
}
private static bool AcknowledgePendingCancellations()
{
if (Task.Current.IsCancellationRequested)
{
Task.Current.AcknowledgeCancellation();
return true;
}
return false;
}
When Cancel()
is called on a Task
, it sets the IsCancellationRequested
property to true, but unless it is acknowledged with AcknowledgeCancellation
, the framework will assume that the Task
completed successfully when the delegate returns. Creating one of these Task
s couldn’t be easier:
_currentTask = ReportableTask.StartNew<int>(OnProgress,
t => _parent.HasProgress = false,
TaskContinuationOptions.OnlyOnRanToCompletion,
PerformOperation,
PerformLongOperation);
In the sample project I have included, cancellation of the Task
is accomplished by clicking on the button that is used to start the Task
s in the first place. You can tell that the Task
s are not completing because when a Task
runs to completion, the OnProgress
method will add the amount of progress to a list of completed operations to output in an ItemsControl
.
The main problem in implementing this as separate sub-tasks is that each sub-task must be atomic. Unfortunately, that is unlikely to be the case when wanting to report progress on some long running operation. The reality is that when we are reporting progress, it is generally to indicate when we have completed a certain aspect of a complex problem that is wholly dependent on the previous work being done.
I’m not sure if there is a better way of doing this using Task
s, but I would love to hear about it if there is.
Here is the source for the post ReportTaskProgress.zip.
-AH