James Curran
James Curran

Reputation: 103505

Using Task.WhenAll with a growing list of Tasks

Task.WhenAll(IEnumerable<Task>) waits for all tasks in the IEnumerable are complete --- but only the tasks in the list when it's first called. If any active task adds to the list, they aren't considered. This short example demonstrates:

    List<Task> _tasks = new List<Task>();

    public async Task  QuickExample()
    {
        for(int n =0; n < 6; ++n)
            _tasks.Add(Func1(n));

        await Task.WhenAll(_tasks);     
        Console.WriteLine("Some Tasks complete");

        await Task.WhenAll(_tasks);
        Console.WriteLine("All Tasks complete");
    }


    async Task Func1(int n)
    {
        Console.WriteLine($"Func1-{n} started");
        await Task.Delay(2000);
        if ((n % 3) == 1)
            _tasks.Add(Func2(n));
        Console.WriteLine($"Func1-{n} complete");
    }

    async Task Func2(int n)
    {
        Console.WriteLine($"Func2-{n} started");
        await Task.Delay(2000);
        Console.WriteLine($"Func2-{n} complete");
    }

This outputs:

Func1-0 started
Func1-1 started
Func1-2 started
Func1-3 started
Func1-4 started
Func1-5 started
Func1-5 complete
Func1-3 complete
Func2-1 started
Func1-1 complete
Func1-0 complete
Func1-2 complete
Func2-4 started
Func1-4 complete
Some Tasks complete
Func2-4 complete
Func2-1 complete
All Tasks complete
Done

The second Task.WhenAll() solves the problem in this case, but that's a rather fragile solution. What's the best way to handle this in the general case?

Upvotes: 2

Views: 4866

Answers (4)

xanatos
xanatos

Reputation: 111860

You are modifying the List<> without locking it... You like to live a dangerous life :-) Save the Count of the _tasks before doing a WaitAll, then after the WaitAll check the Count of _tasks. If it is different, do another round (so you need a while around the WaitAll.

int count = _tasks.Count;

while (true)
{
    await Task.WhenAll(_tasks);

    lock (_tasks)
    {
        if (count == _tasks.Count)
        {
            Console.WriteLine("All Tasks complete");
            break;
        }

        count = _tasks.Count;
        Console.WriteLine("Some Tasks complete");
    }
}

async Task Func1(int n)
{
    Console.WriteLine($"Func1-{n} started");
    await Task.Delay(2000);

    if ((n % 3) == 1)
    {
        lock (_tasks)
        {
            _tasks.Add(Func2(n));
        }
    }

    Console.WriteLine($"Func1-{n} complete");
}

I'll add a second (probably more correct solution), that is different from what you are doing: you could simply await the new Tasks from the Tasks that generated them, without cascading them to the _tasks collection. If A creates B, then A doesn't finish until B finishes. Clearly you don't need to add the new Tasks to the _tasks collection.

Upvotes: 2

John Wu
John Wu

Reputation: 52240

Since it seems that additional tasks can be created during the course of executing the original list of tasks, you will need a simple while construct.

while (_tasks.Any( t => !t.IsCompleted ) 
{
    await Task.WhenAll(_tasks);
}

This will check the list for any uncompleted tasks and await them until it catches the list at a moment when there are no tasks left.

Upvotes: 1

Fabio
Fabio

Reputation: 32445

Asynchronous function will return to the caller on first await.
So for loop will be complete before you add extra tasks to original tasks list.

Implementation of Task.WhenAll will iterate/copy tasks to local list, so added tasks after Task.WhenAll called will be ignored.

In your particular case moving call to Func1 before await Task.Delay() could be a solution.

async Task Func1(int n)
{
    Console.WriteLine($"Func1-{n} started");
    if ((n % 3) == 1)
        _tasks.Add(Func2(n));

    await Task.Delay(2000);
    Console.WriteLine($"Func1-{n} complete");
}

But if in real scenario calling of Func2 depend on result of some asynchronous method, then you need some other solution.

Upvotes: 1

Breealzibub
Breealzibub

Reputation: 8095

Consider this; it sounds like work is being submitted to the "Task List" from another thread. In a sense, the "task submission" thread itself could also be yet another Task for you to wait on.

If you wait for all Tasks to be submitted, then you are guaranteed that your next call to WhenAll will yield a fully-completed payload.

Your waiting function could/should be a two-step process:

  1. Wait for the "Task Submitting" task to complete, signalling all Tasks are submitted
  2. Wait for all the submitted tasks to complete.

Example:

public async Task WaitForAllSubmittedTasks()
{
    // Work is being submitted in a background thread;
    // Wrap that thread in a Task, and wait for it to complete.
    var workScheduler = GetWorkScheduler();
    await workScheduler;

    // All tasks submitted!

    // Now we get the completed list of all submitted tasks.
    // It's important to note: the submitted tasks
    // have been chugging along all this time.
    // By the time we get here, there are probably a number of
    // completed tasks already.  It does not delay the speed
    // or execution of our work items if we grab the List
    // after some of the work has been completed.
    //
    // It's entirely possible that - by the time we call
    // this function and wait on it - almost all the 
    // tasks have already been completed!
    var submittedWork = GetAllSubmittedTasks();
    await Task.WhenAll(submittedWork);

    // Work complete!
}

Upvotes: 0

Related Questions