当线程池的可用工作线程总数为零时,线程池从哪里获取新线程?

Where does the thread pool get new threads from when its total available worker threads has reached zero?

为了好玩,我写了这段代码来模拟死锁。然后,我坐着耐心地看着它运行,直到线程池的可用工作线程总数下降到零。我很好奇会发生什么。它会抛出异常吗?

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 任务管理器的性能选项卡 运行ning 并观察操作系统线程总数随着我的程序消耗更多线程而增加。

这是我观察到的:

  1. OS 没有创建更多线程,因为 .NET 线程池每次都创建一个线程。事实上,对于我的 for 循环 运行 每四到五次迭代,OS 线程数就会增加一或两个。这很有趣,但这不是我的问题。证明了.

  2. 更有趣的是,我观察到在我的 for 循环的每次迭代中线程数并没有减少 2。我预计它应该下降 2 因为 none 我的死锁线程预计 return 因为它们死锁,互相等待。

  3. 我还观察到,当线程池中可用的工作线程总数下降到零时,程序仍然保持 运行ning 循环的更多迭代。这让我很好奇 这些新线程来自哪里 如果线程池已经 运行 没有线程并且 none 线程有 returned?

所以,为了澄清,我的两个问题可能是相关的,因为一个答案可能是对它们的解释,它们是:

  1. 当我的 for 循环 运行 的单次迭代时,对于其中一些迭代,没有创建线程池线程。为什么?线程池从哪里获取线程以进行 运行 这些迭代?

  2. 线程池从哪里获取线程运行超出其可用工作线程总数并且仍然保持运行宁我的for循环?

线程池(几乎)永远不会 运行 没有线程。有一种注入启发式方法,当它认为这有助于提高吞吐量时,它会以(几乎)无限制的方式添加新线程。这也可以防止由于可用线程太少而导致的死锁。

This can be a big problem because memory usage is (almost) unbounded.

"Almost" 因为有最大线程数,但在实践中往往非常高(数千个线程)。

When a single iteration of my for-loop ran, for some of those iterations, no thread pool threads were created.

从显示的数据来看,原因对我来说并不明显。您可能应该在每次迭代后测量 Process.GetCurrentProcess().ThreadCount

也许在某些情况下避免了死锁?这不是确定性死锁。

在当前的 CLR 运行ning 线程似乎是 1:1 和 OS 个线程。

也许您应该 运行 一个更简单的基准?

        for (int i = 0; i < 10000000; i++)
        {
            Task.Run(() => Thread.Sleep(Timeout.Infinite));

            int workerThreads;
            int cpThreads;
            ThreadPool.GetAvailableThreads(out workerThreads, out cpThreads);

            Console.WriteLine($"Queued: {i}, threads: {Process.GetCurrentProcess().Threads.Count}, workerThreads: {workerThreads}, workerThreads: {cpThreads}");

            Thread.Sleep(100);
        }

原因是 QueueUserWorkItem: "Queues a method for execution. The method executes when a thread pool thread becomes available." https://msdn.microsoft.com/en-us/library/kbf0f1ct(v=vs.110).aspx
在我的理解中,Threadpool 只是慢慢增加线程数以满足您的需求,这就是您在 taskmgr 中看到的。我想你可以通过在你的线程中添加一些要完成的事情来验证这一点。
编辑:我的意思是,你只是将它们排队,第一个线程开始,然后慢慢地(每 500 毫秒,https://blogs.msdn.microsoft.com/pedram/2007/08/05/dedicated-thread-or-a-threadpool-thread/)添加越来越多的线程,直到达到限制 - 之后你仍然可以排队新线程。

    ThreadPool.GetAvailableThreads(out worker, out io);

这不是一个很好的统计数据来向您展示线程池的工作原理。主要问题是它在所有最新的 .NET 版本中都大得离谱。在我的双核笔记本电脑上,它在 32 位模式下为 1020,在 64 位模式下为 32767。远远地,far 比这样一个贫血的 CPU 可以合理处理。多年来,这个数字显着膨胀,最初是 .NET 2.0 中内核数量的 50 倍。它现在是根据机器能力动态计算的,这是 CLR 主机的工作。它使用超过半满的玻璃杯。

线程池管理器的主要工作是保持线程高效。最佳点是将执行线程的数量限制在处理器内核的数量之内。 运行 更多地降低性能,OS 然后必须在线程之间进行上下文切换,这会增加开销。

然而,这个理想并不总能得到满足,程序员编写的实际 tp 线程并不总是表现良好。在实践中,他们花了太长时间 and/or 花太多时间阻塞 I/O 或锁而不是执行代码。你的例子当然是一个相当极端的阻塞案例。

线程池管理器无法确切地知道为什么 tp 线程的执行时间太长。它只能看到它需要太长时间才能完成。深入了解线程花费太长时间的确切原因是不切实际的,它需要一个调试器和程序员耳边传来的那种经过大量训练的大规模并行神经网络。

线程池管理器每秒两次重新评估工作负载,并允许 extra tp 线程在 none 个活动线程完成时启动。即使那超出了最佳状态。从理论上讲,这可能会完成更多的工作,因为大概活动的那些阻塞太多并且没有有效地使用可用的核心。解决一些死锁场景也很重要,尽管你never want to need that。它只是一个像其他任何线程一样的常规线程,底层 OS 调用是 CreateThread()。

这就是您所看到的,可用线程数每秒减少一个。独立于您的代码,这是基于时间的。实际上在管理器中实现了一个反馈循环,试图动态计算额外线程的最佳数量。你还没有到达那里,所有线程都阻塞了。

这不会永远持续下去,您最终会达到默认 SetMaxThreads() 设置的上限。也不例外,假设您没有首先遇到 OutOfMemoryException 并且您在现实生活中经常遇到,它只是停止添加更多线程。您仍在向线程池添加 执行请求,涵盖第 3 点,它们只是从未真正开始。当请求数量太大时,最终你会 运行 内存不足。等的时间很长,1GB的容量都需要一段时间