启动多个阻塞任务的奇怪行为

Strange behavior of starting multiple tasks that block

这是一个启动(无需等待)100 个任务的测试方法,每个任务在 BlockingCollection 上调用 GetConsumingEnumerable。 (Update:下面描述的行为并不特定于此方法;它可以是任何同步阻塞方法调用。)我想了解为什么在前 10 个任务并行启动后,后续任务按顺序开始,每个任务在下一个任务之前几乎正好等待 1000 毫秒:

[TestMethod]
public void Test()
{
    BlockingCollection<int> list = new();
    Console.WriteLine(DateTime.UtcNow + @": " + "START");

    for (int i = 0; i < 100; i++)
    {
        int x = i;
        Task.Run(() =>
        {
            Console.WriteLine($"{DateTime.UtcNow}: Starting {x}");

            // This will just block
            foreach (int item in list.GetConsumingEnumerable())
            {
                Console.WriteLine($"{DateTime.UtcNow}: foo {x}");
            }

            // We'll never get here:
            Console.WriteLine($"{DateTime.UtcNow}: Finishing {x}");
        });
    }
    Console.WriteLine(DateTime.UtcNow + @": " + "END");
    // Just to give the test enough time to print all messages
    Thread.Sleep(100_000);
}

这是输出的前 26 行:

02.02.2021 12:17:41: START
02.02.2021 12:17:41: END
02.02.2021 12:17:41: Starting 9
02.02.2021 12:17:41: Starting 0
02.02.2021 12:17:41: Starting 1
02.02.2021 12:17:41: Starting 6
02.02.2021 12:17:41: Starting 4
02.02.2021 12:17:41: Starting 2
02.02.2021 12:17:41: Starting 5
02.02.2021 12:17:41: Starting 7
02.02.2021 12:17:41: Starting 3
02.02.2021 12:17:41: Starting 8
02.02.2021 12:17:41: Starting 10
02.02.2021 12:17:42: Starting 11
02.02.2021 12:17:43: Starting 12
02.02.2021 12:17:44: Starting 13
02.02.2021 12:17:45: Starting 14
02.02.2021 12:17:45: Starting 15
02.02.2021 12:17:46: Starting 16
02.02.2021 12:17:47: Starting 17
02.02.2021 12:17:48: Starting 18
02.02.2021 12:17:49: Starting 19
02.02.2021 12:17:50: Starting 20
02.02.2021 12:17:51: Starting 21
02.02.2021 12:17:52: Starting 22
02.02.2021 12:17:53: Starting 23
02.02.2021 12:17:54: Starting 24

前几行符合预期。但是为什么它在任务 #10 之后开始等待 1000 毫秒?我的第一个假设是,由于 GetConsumingEnumerable 阻塞了线程,也许线程池中的线程在第 10 个任务之后就全部用完了,但这并不能解释 1000 毫秒的延迟。

[感谢 Jon Skeet 和 Theodor Zoulias 的评论]

由于GetConsumingEnumerable阻塞线程,在第10个任务有运行后,线程池中所有可用的空闲线程都被阻塞。这与 GetConsumingEnumerable 具体无关:每当任务阻塞时都会发生相同的行为(例如,用 Thread.Sleep(Timeout.Infinite) 替换调用)。

如线程池documentation中所述,扩展线程池延迟:

As part of its thread management strategy, the thread pool delays before creating threads. Therefore, when a number of tasks are queued in a short period of time, there can be a significant delay before all the tasks are started.

要确认这一行为,可以使用

增加按需线程的最小数量
ThreadPool.SetMinThreads(1000, 1000);

使用此设置,所有 100 个任务 运行 都不会延迟。