Stuck at Task.WaitAll(tasks.ToArray()) while using Task.Start to trigger the tasks

Viewed 305

We had something like below

List<string> uncheckItems = new List<string>();
for (int i = 0; i < 100; i++)
{
    uncheckItems.Add($"item {i + 1}");
}

var tasks = uncheckItems.Select(item =>
    new Task(async () => await ProcessItem(item))
);

// Do some preparations

foreach (var task in tasks)
{
    task.Start();
}

Task.WaitAll(tasks.ToArray());
Console.WriteLine("=====================================================All finished");

It seems to make sense but the program never able to reach the all finished line. And if I adjust the workflow to run tasks immediately like remove the task.Start() loop and change to

var tasks = uncheckItems.Select(async item =>
    await ProcessItem(item)
);

Then it works.

However, I wonder

  1. Why it stucks?
  2. Is there any way we can keep the workflow(create tasks without trigger them directly and start them later on) and still able to utilize WaitAll()?
2 Answers

The reason is the lazy enumeration evaluation, you are starting different tasks than waiting with Task.WaitAll. This can be fixed for example with next:

var tasks = uncheckItems.Select(item =>
    new Task(async () => await ProcessItem(item))
)
.ToArray();

Though it will not achieve your goal (as I understand) of waiting all ProcessItem to finish. You can do something like new Task(() => ProcessItem(item).GetAwaiter().GetResult()) but I think it would be better to change your approach, for example make ProcessItem return a "cold" task or using your second snippet and moving tasks creation to the point where they needed to be started.

You should be next to the world expert in Task to be using the constructor. The documentation warns against that:

This constructor should only be used in advanced scenarios where it is required that the creation and starting of the task is separated.

Rather than calling this constructor, the most common way to instantiate a Task object and launch a task is by calling the static Task.Run(Action) or TaskFactory.StartNew(Action) method.

If a task with no action is needed just for the consumer of an API to have something to await, a TaskCompletionSource should be used.

The Task constructor produces a non-started Task that will only start when Task.Start() is invoked, as you discovered.

The Task constructor also receives an Action (or Action<T>), so the return value is ignored. That means that, after started, the task will end as soon as async () => await ProcessItem(item) yields.

What you need is:

await Task.WhenAll(Enumerable.Range(0, 100).Select(i => ProcessItem($"item {i + 1}"));

Or, if you really have to block:

Task
    .WhenAll(Enumerable.Range(0, 100).Select(i => ProcessItem($"item {i + 1}"))
    .GetAwaiter().GetResult();
Related