为了好玩,我写了这段代码来模拟死锁。然后,我坐着耐心地看着它运行,直到线程池的可用工作线程总数降为零。我很好奇会发生什么。它会抛出异常吗?
using System;
using System.Diagnostics;
using System.Threading;
namespace Deadlock
{
class Program
{
private static readonly object lockA = new object();
private static readonly object lockB = new object();
static void Main(string[] args)
{
int worker, io;
ThreadPool.GetAvailableThreads(out worker, out io);
Console.WriteLine($"Total number of thread pool threads: {worker}, {io}");
Console.WriteLine($"Total threads in my process: {Process.GetCurrentProcess().Threads.Count}");
Console.ReadKey();
try
{
for (int i = 0; i < 1000000; i++)
{
AutoResetEvent auto1 = new AutoResetEvent(false);
AutoResetEvent auto2 = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(ThreadProc1, auto1);
ThreadPool.QueueUserWorkItem(ThreadProc2, auto2);
var allCompleted = WaitHandle.WaitAll(new[] { auto1, auto2 }, 20);
ThreadPool.GetAvailableThreads(out worker, out io);
var total = Process.GetCurrentProcess().Threads.Count;
if (allCompleted)
{
Console.WriteLine($"All threads done: (Iteration #{i + 1}). Total: {total}, Available: {worker}, {io}\n");
}
else
{
Console.WriteLine($"Timed out: (Iteration #{i + 1}). Total: {total}, Available: {worker}, {io}\n");
}
}
Console.WriteLine("Press any key to exit...");
}
catch(Exception ex)
{
Console.WriteLine("An exception occurred.");
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
Console.WriteLine("The program will now exit. Press any key to terminate the program...");
}
Console.ReadKey();
}
static void ThreadProc1(object state)
{
lock(lockA)
{
Console.WriteLine("ThreadProc1 entered lockA. Going to acquire lockB");
lock(lockB)
{
Console.WriteLine("ThreadProc1 acquired both locks: lockA and lockB.");
//Do stuff
Console.WriteLine("ThreadProc1 running...");
}
}
if (state != null)
{
((AutoResetEvent)state).Set();
}
}
static void ThreadProc2(object state)
{
lock(lockB)
{
Console.WriteLine("ThreadProc2 entered lockB. Going to acquire lockA.");
lock(lockA)
{
Console.WriteLine("ThreadProc2 acquired both locks: lockA and lockB.");
// Do stuff
Console.WriteLine("ThreadProc2 running...");
}
}
if (state != null)
{
((AutoResetEvent)state).Set();
}
}
}
}
与此同时,我还让 Windows 任务管理器的性能选项卡保持运行,并观察操作系统线程总数随着我的程序消耗更多线程而增加。
这是我观察到的:
操作系统没有创建更多线程,因为 .NET 线程池每次都创建一个线程。事实上,对于我的
for
循环运行的每四到五次迭代,操作系统线程数就会增加一到两个。这很有趣,但这不是我的问题。证明what has already been established .更有趣的是,我发现在我的
for
循环的每次迭代中,线程数并没有减少 2。我预计它应该下降 2,因为我的死锁线程预计不会返回,因为它们处于死锁状态,相互等待。我还观察到,当线程池中的可用工作线程总数下降到零时,程序仍然继续运行我的 for 循环的更多迭代。这让我很好奇,如果线程池已经用完线程并且没有线程返回,那么那些新线程来自哪里?
所以,为了澄清,我的两个问题可能是相关的,因为一个答案可能是对它们的解释,它们是:
当我的 for 循环运行单次迭代时,对于其中一些迭代,没有创建线程池线程。为什么?线程池从哪里获取线程来运行这些迭代?
当线程池用完其可用工作线程总数并仍在运行我的
for
循环时,它从哪里获取线程?
最佳答案
ThreadPool.GetAvailableThreads(out worker, out io);
这不是一个很好的统计数据来向您展示线程池的工作原理。主要问题是它在所有最新的 .NET 版本中都大得离谱。在我的双核笔记本电脑上,它在 32 位模式下为 1020,在 64 位模式下为 32767。比这样一个贫血的 CPU 可以合理处理的大得多,远。多年来,这个数字显着膨胀,最初是 .NET 2.0 中内核数量的 50 倍。它现在是根据机器能力动态计算的,这是 CLR 主机的工作。它使用半满的玻璃杯。
线程池管理器的主要工作是保持线程高效。最佳点是将执行线程的数量限制在处理器内核的数量之内。运行更多会降低性能,然后操作系统必须在线程之间进行上下文切换,这会增加开销。
然而,这个理想并不总能得到满足,程序员编写的实际 tp 线程并不总是表现良好。实际上,它们花费的时间太长和/或花太多时间阻塞 I/O 或锁而不是执行代码。你的例子当然是一个相当极端的阻塞案例。
线程池管理器不知道为什么 tp 线程执行的时间太长。它只能看到它需要太长时间才能完成。深入了解线程花费太长时间的确切原因是不切实际的,它需要调试器和程序员耳边所熟悉的那种经过大量训练的大规模并行神经网络。
每秒两次,线程池管理器重新评估工作负载并允许一个额外 tp 线程在没有事件线程完成时启动。即使那超出了最佳状态。从理论上讲,这可能会完成更多的工作,因为大概事件的那些阻塞太多并且没有有效地使用可用的核心。解决一些死锁情况也很重要,尽管你 never want to need that .它与任何其他线程一样只是一个常规线程,底层操作系统调用是 CreateThread()。
这就是您所看到的,可用线程数每秒两次减少一个。独立于您的代码,这是基于时间的。实际上在管理器中实现了一个反馈循环,试图动态计算额外线程的最佳数量。你还没有到达那里,所有线程都阻塞了。
这不会永远持续下去,您最终会达到默认 SetMaxThreads() 设置的上限。也不异常(exception),假设您没有首先遇到 OutOfMemoryException 并且您在现实生活中经常遇到,它只是停止添加更多线程。您仍在向线程池添加执行请求,涵盖第 3 点,它们只是从未真正开始。当请求数量太大时,您最终会耗尽内存。您将不得不等待很长时间,需要一段时间才能填满 1 GB。
关于c# - 当线程池的可用工作线程总数为零时,线程池从哪里获取新线程?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37544816/