Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Showing progress while waiting for all Tasks in List<Task> to complete

I'm currently trying to continuously print dots at the end of a line as a form of indeterminate progress, while a large list of Tasks are running, with this code:

start = DateTime.Now;
Console.Write("*Processing variables");
Task entireTask = Task.WhenAll(tasks);
Task progress = new Task(() => { while (!entireTask.IsCompleted) { Console.Write("."); System.Threading.Thread.Sleep(1000); } });
progress.Start();
entireTask.Wait();
timeDiff = DateTime.Now - start;
Console.WriteLine("\n*Operation completed in {0} seconds.", timeDiff.TotalSeconds);

Where tasks is from List<Task> tasks = new List<Task>();,
and tasks.Add(Task.Run(() => someMethodAsync())); has occurred 10000's of times.
This code currently works, however, is this the correct way of accomplishing this, and is this the most cost-effective way?

like image 262
cloudcrypt Avatar asked Mar 22 '16 08:03

cloudcrypt


2 Answers

There are certainly several ways this can be solved and one of them is yours. However it is not really a good practice to start long running tasks especially when they do nothing than synchronous waiting (that is Thread.Sleep).

You should consider refactoring your code in a technical and a domain part. The technical part is:

  1. Wait until all Tasks in a given collection have completed
  2. If that takes longer do regular progress reports

The following code might help to understand this a bit better. It starts four tasks which simulate different async operations and waits for all of them to complete. If this takes longer than 250ms the call of WhenAllEx keeps on calling a lambda for reoccuring progress report.

static void Main(string[] args)
{
    var tasks = Enumerable.Range(0, 4).Select(taskNumber => Task.Run(async () =>
    {
        Console.WriteLine("Task {0} starting", taskNumber);
        await Task.Delay((taskNumber + 1) * 1000);
        Console.WriteLine("Task {0} stopping", taskNumber);
    })).ToList();

    // Wait for all tasks to complete and do progress report
    var whenAll = WhenAllEx(
        tasks, 
        _ => Console.WriteLine("Still in progress. ({0}/{1} completed)", _.Count(task => task.IsCompleted), tasks.Count()));

    // Usually never wait for asynchronous operations unless your in Main
    whenAll.Wait();
    Console.WriteLine("All tasks finished");
    Console.ReadKey();
}

/// <summary>
/// Takes a collection of tasks and completes the returned task when all tasks have completed. If completion
/// takes a while a progress lambda is called where all tasks can be observed for their status.
/// </summary>
/// <param name="tasks"></param>
/// <param name="reportProgressAction"></param>
/// <returns></returns>
public static async Task WhenAllEx(ICollection<Task> tasks, Action<ICollection<Task>> reportProgressAction)
{
    // get Task which completes when all 'tasks' have completed
    var whenAllTask = Task.WhenAll(tasks);
    for (; ; )
    {
        // get Task which completes after 250ms
        var timer = Task.Delay(250); // you might want to make this configurable
        // Wait until either all tasks have completed OR 250ms passed
        await Task.WhenAny(whenAllTask, timer);
        // if all tasks have completed, complete the returned task
        if (whenAllTask.IsCompleted)
        {
            return;
        }
        // Otherwise call progress report lambda and do another round
        reportProgressAction(tasks);
    }
}
like image 77
Thomas Zeman Avatar answered Oct 15 '22 21:10

Thomas Zeman


As Thomas mentioned, there are certainly several ways to handle this. The one that springs immediately to mind for me is:

start = DateTime.Now;
Console.Write("*Processing variables");
Task entireTask = Task.WhenAll(tasks);
while (await Task.WhenAny(entireTask, Task.Delay(1000)) != entireTask)
{
  Console.Write(".");
}
timeDiff = DateTime.Now - start;
Console.WriteLine("\n*Operation completed in {0} seconds.", timeDiff.TotalSeconds);

Note that this approach does use await, thus requiring this method to be async. Usually for console apps, I recommend having a Main just call MainAsync, so your blocking (or main loop) is all in one line of code and not mixed with any logic.

like image 37
Stephen Cleary Avatar answered Oct 15 '22 22:10

Stephen Cleary