启动多个阻塞任务的奇怪行为
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 个任务 运行 都不会延迟。
这是一个启动(无需等待)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 个任务 运行 都不会延迟。