Reputation: 103505
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
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 Task
s from the Task
s 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 Task
s to the _tasks
collection.
Upvotes: 2
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
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
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:
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