How can I get a progress report on a async function with many tasks, in which I am using await Task.WhenAll(tasks);

405 views Asked by At

I already read all those posts:

Reporting progress from Async Task

https://devblogs.microsoft.com/dotnet/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/

https://blog.stephencleary.com/2012/02/reporting-progress-from-async-tasks.html

But still I don't know how to report a Progress from many Tasks when I'm using await Task.WhenAll(tasks);

So, I am using this code:

public string StatusMessage { get; set; }
public async void ButtonClickHandler(object sender, EventArgs e)
{
    var tasks = new List<Task>
    {
        Task1,
        Task2,
        Task3,
        Task4,
        Task5
    };
    await Task.WhenAll(tasks);
}

So I want to get a change in StatusMessage when any on Task1..Task5 is completed. Somehow each task has to have a name and that name to be reported to StatusMessage variable.

I want also an event to rise when StatusMessage changes.

StatusMessage could be also a cumulative list or barely a string.

JSteward post from here it is interesting but I don't know how to convert my Task1..Task5 tasks in a WorkItem from his sample.

How can I do that?

2

There are 2 answers

2
Theodor Zoulias On BEST ANSWER

You could define an extension method on the Task type, that reports progress on an IProgress<T> when the task completes successfully:

public static async Task OnSuccessfulCompletion<T>(
    this Task task, IProgress<T> progress, T progressValue)
{
    ArgumentNullException.ThrowIfNull(task);
    ArgumentNullException.ThrowIfNull(progress);

    await task.ConfigureAwait(false);
    progress.Report(progressValue);
}

And use it like this:

public async void ButtonClickHandler(object sender, EventArgs e)
{
    /* Launching the tasks Task1, Task2 etc is omitted */
    Progress<string> progress = new(value => textBox1.AppendText($"{value}\r\n"));
    List<Task> tasks = new()
    {
        Task1.OnSuccessfulCompletion(progress, "Task1"),
        Task2.OnSuccessfulCompletion(progress, "Task2"),
        Task3.OnSuccessfulCompletion(progress, "Task3"),
        Task4.OnSuccessfulCompletion(progress, "Task4"),
        Task5.OnSuccessfulCompletion(progress, "Task5"),
    };
    await Task.WhenAll(tasks);
}

Be aware that the Progress<T> type reports progress asynchronously. This means that the handler might be invoked after the completion of the ButtonClickHandler method. If you write anything directly on the textBox1, immediately after the line await Task.WhenAll(tasks);, this text might not be the last to be appended to the textBox1. In case you don't like this behavior, you can find a synchronous IProgress<T> implementation here.

0
JonasH On

The standard IProgress<T> is a bit limiting if you want to do more detailed progress reporting. There is no easy way to include features like description or to distribute the work over multiple independent tasks.

My preference is to replace this with a custom progress class, something like

public class Progress
{
    // object should *either* have value+description, *or* subProgress list
    private double value;
    private string description = "";
    private Progress[] subProgress;

    // progress should always be between 0-1 to keep math simple
    public double Value
    {
        get
        {
            var subProgressLocal = subProgress;
            if (subProgress != null)
            {
                return subProgressLocal.Sum(s => s.Value ) / subProgressLocal.Length;
            }

            return value;
        }
    }

    public string Description
    {
        get
        {
            var subProgressLocal = subProgress;
            if (subProgressLocal != null)
            {
                return subProgressLocal.FirstOrDefault(p => p.Value is > 0d and < 1d)?.Description ?? "";
            }

            return description;
        }
    }

    public Progress[] CreateSubProgress(int count)
    {
        if (subProgress != null)
            throw new InvalidOperationException("Can only create sub-progresses once");
        subProgress = Enumerable.Range(0, count).Select(_ => new Progress()).ToArray();
        return subProgress;
    }

    // progress should always be between 0-1 to keep math simple
    public void Report(double progress, string description = "")
    {
        if (subProgress != null)
            throw new InvalidOperationException("Cannot report progress when using sub-progress");
        // Clamp to 0-1 range
        this.value = progress < 0 ? 0 : (progress > 1 ? 1 : progress);
        this.description = description;
    }
}

This allow you to build a "tree" that matches the computations you are doing, where all reporting is done on the leaf nodes. So you can have multiple nested loops while still getting detailed progress reporting, and without needing to know the total amount of work before you start.

This does not raise any event on the UI thread when a task reports progress. So you will need to use a UI compatible timer to periodically poll the progress and update the UI. Also note that this is not strictly thread safe, since shared variables will be read and written to from different threads, but the worst that could happen should be a slightly lagging progress bar. Also note that this is a "one time use"-object, create a new one each time you need progress reporting.

Usage like:

void DoMultiStepWork(Progress p){
    var subprogress = p.CreateSubProgress(3);
    DoPart1(subProgress[0]);
    DoPart2(subProgress[1]);
    DoPart3(subProgress[2]);
}
void DoPart1(Progress p){
    for(int i = 0; i < myCollection.Count; i++){
       // do work
       p.Report(i / (double)myCollection.Count, myCollection[i].Name);
    }
}

You can write extension methods to make usage more convenient, like foreach(var item in myCollection.ProgressReporting(p)). Or add features like weights for each sub-progress, to handle cases with uneven work distribution.

This should work even if each method is run on different tasks or threads.