Reporting progress from a Task

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 Tasks are aggregated by the parent Task, and can share the same CancellationToken so that if the main Task is cancelled, the child Tasks 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 Tasks, 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 Tasks 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 Tasks in the first place. You can tell that the Tasks 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 Tasks, but I would love to hear about it if there is.

Here is the source for the post ReportTaskProgress.zip.

-AH