当线程池的总可用工作线程达到零时,线程池从何处获取新线程?

时间:2016-05-31 11:43:09

标签: c# .net multithreading clr

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

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任务管理器的性能选项卡运行,并观察操作系统线程的总数,因为我的程序占用了更多的线程。

这是我观察到的:

  1. 操作系统没有创建更多线程,因为.NET线程池每次都创建一个线程。实际上,对于我的for循环运行的每四到五次迭代,操作系统线程数将增加一或两个。这很有趣,但这不是我的问题。它证明what has already been established

  2. 更有趣的是,我观察到在for循环的每次迭代中线程数没有减少2。我预计它应该已经下降了2,因为我的死锁线程没有预期返回,因为它们已经陷入僵局,彼此等待。

  3. 我还观察到,当线程池中可用工作线程的总数减少到零时,程序仍然继续运行我的for循环的更多迭代。这让我很好奇如果线程池已经用完线程并且没有线程返回,那些新线程来自

  4. enter image description here

    enter image description here

    因此,为了澄清,我的两个问题,或许与一个答案相关,可能是对他们的解释,是:

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

    2. 当线程池用完可用工作线程总数并仍然继续运行for循环时,线程池从何处获取?

3 个答案:

答案 0 :(得分:5)

    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。它只是一个常规线程,就像任何其他的一样,底层的OS调用是CreateThread()。

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

这不会永远持续下去,您最终会达到默认SetMaxThreads()设置的上限。没有例外,假设您没有首先遇到OutOfMemoryException并且您在现实生活中经常遇到,它只会停止添加更多线程。您仍在向线程池添加执行请求,涵盖了子弹3,它们实际上从未真正开始。当请求数量过大时,最终会耗尽内存。你需要等待很长时间,需要一段时间才能填满一个千兆字节。

答案 1 :(得分:3)

原因是QueueUserWorkItem:&#34;将一个方法排队执行。该方法在线程池线程可用时执行。&#34; 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/)添加越来越多的线程,直到达到限制 - 之后你仍然可以排队新线程。

答案 2 :(得分:1)

线程池(几乎)永远不会用完线程。有一种注入试探法,当它认为这有助于吞吐量时,它会以(几乎)无限制的方式添加新线程。基于可用的线程太少,这也可以防止死锁。

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

&#34;几乎&#34;因为有一个最大的线程数,但在实践中往往非常高(数千个线程)。

  

当我的for循环的单次迭代运行时,对于其中一些迭代,没有创建线程池线程。

根据显示的数据,我不明白这个原因。您可能应该在每次迭代后测量Process.GetCurrentProcess().ThreadCount

在某些情况下可能会避免死锁?这不是一个确定性的僵局。

在当前的CLR上,运行线程与OS线程似乎是1:1。

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

        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);
        }

enter image description here