我重写了发布的代码片段,以便更好地排序结果,我的全新笔记本电脑有太多内核,无法很好地解释现有的混乱输出。记录每个任务的开始和结束时间,并在完成后显示它们。并记录任务的实际开始时间。我有:
0: 68 - 5031
1: 69 - 5031
2: 68 - 5031
3: 69 - 5031
4: 69 - 1032
5: 68 - 5031
6: 68 - 5031
7: 69 - 5031
8: 1033 - 5031
9: 1033 - 2032
10: 2032 - 5031
11: 2032 - 3030
12: 3030 - 5031
13: 3030 - 4029
14: 4030 - 5031
15: 4030 - 5031
啊,这突然很有意义。处理线程池线程时始终要注意的模式。请注意,每秒会发生一些重大事件,两个 tp 线程开始运行,其中一些线程可以完成。
这是一个死锁场景,类似于this Q+A https://stackoverflow.com/questions/25907829/why-is-parallel-foreach-much-faster-then-asparallel-forall-even-though-msdn但除此之外,该用户的代码不会产生更灾难性的结果。原因几乎不可能看到,因为它隐藏在 .NETFramework 代码中,您必须查看 Task.Delay() 是如何实现的才能理解它。
相关代码is here https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,5896,注意它如何使用 System.Threading.Timer 来实现延迟。关于该计时器的一个重要细节是它的回调是在线程池上执行的。这是 Task.Delay() 实现“不使用不用付费”承诺的基本机制。
重要的细节是,如果线程池正忙于处理线程池执行请求,这可能需要一段时间。并不是计时器慢,问题是回调方法启动得不够快。这个程序中的问题是,Task.Run()添加了一堆请求,超过了可以同时执行的请求。发生死锁是因为由 Task.Run() 启动的 tp 线程在计时器回调执行之前无法完成 Wait() 调用。
通过将这段代码添加到 Main() 的开头,可以使其成为永久挂起程序的硬死锁:
ThreadPool.SetMaxThreads(Environment.ProcessorCount, 1000);
但正常的最大线程要高得多。线程池管理器利用它来解决这种死锁。每秒一次,当现有线程未完成时,它允许比“理想”数量多两个线程执行。这就是您在输出中看到的内容。但一次只有两个,不足以对在 Wait() 调用上阻塞的 8 个繁忙线程产生太大影响。
Thread.Sleep() 调用没有这个问题,它不依赖于.NETFramework 代码或线程池来完成。操作系统线程调度程序负责处理它,并且它始终依靠时钟中断运行。因此,允许新的 tp 线程每 100 或 300 毫秒开始执行一次,而不是每秒一次。
很难给出具体建议来避免这种僵局陷阱。除了通用建议之外,始终避免工作线程阻塞。