使用 Task.Start 触发任务时卡在 Task.WaitAll(tasks.ToArray())

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

我们有如下内容

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");

这似乎是有道理的,但程序始终无法到达全部完成的行。 如果我立即将工作流程调整为 运行 任务,例如删除 task.Start() 循环并更改为

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

然后就可以了。

不过,我想知道

  1. 为什么会卡住?
  2. 有什么方法可以保持工作流程(创建任务而不直接触发它们并在稍后启动它们)并且仍然能够利用 WaitAll()

原因是 lazy enumeration evaluation,您开始的任务与等待 Task.WaitAll 的任务不同。这可以通过 next:

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

尽管它不会实现您等待所有 ProcessItem 完成的目标(据我了解)。你可以做类似 new Task(() => ProcessItem(item).GetAwaiter().GetResult()) 的事情,但我认为最好改变你的方法,例如使 ProcessItem return 成为“冷”任务或使用你的第二个片段并将任务创建移动到他们需要开始的地方。

您应该仅次于 Task 的世界专家来使用构造函数。 documentation 警告:

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.

Task 构造函数生成一个未启动的 Task,正如您发现的那样,它只会在调用 Task.Start() 时启动。

Task 构造函数还接收一个 Action(或 Action<T>),因此忽略 return 值。这意味着,启动后,任务将在 async () => await ProcessItem(item) 产生后立即结束。

您需要的是:

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

或者,如果您真的必须阻止:

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