4

Taken from an answer to one of my previous questions (Task.Factory.StartNew starts with a great delay despite having available threads in threadpool):

"It's not the MAX worker threads value you need to look at - it's the MIN value you get via ThreadPool.GetMinThreads(). The max value is the absolute maximum threads that can be active. The min value is the number to always keep active. If you try to start a thread when the number of active threads is less than max (and greater than min) you'll see a 2 second delay."

So, I've prepared a sample code to test it:

ThreadPool.GetMinThreads() returns "8" for my machine, and I run the code on my local machine.

My code looks as following:

        Task task = null;

        int workerThreads = 0;
        int completionPortThreads = 0;



        ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);
        Logger.WriteInfo(LogCode.EMPTY_INFO,  workerThreads.ToString());**returns "8"**

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread1");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread1");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread2");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread2");
            while (true)
            {
                DoSomthing();
            }
        });
    ;
        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread3");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread3");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread4");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread4");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread5");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread5");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread6");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread6");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread7");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread7");
            while (true)
            {
                DoSomthing();
            }
        });


        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread8");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread8");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread9");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread9");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread10");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread10");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread11");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread11");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread12");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread12");
            while (true)
            {
                DoSomthing();
            }
        });

        Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread13");
        task = Task.Factory.StartNew(() =>
        {
            Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread13");
            while (true)
            {
                DoSomthing();
            }
        });


private void DoSomthing()
    {
        int j = 1;
        for (int i = 0; i < 2000; i++)
        {
            j = i * i;
        }
    }

The Logger class just uses log4net.dll. So, ThreadPool.GetMinThreads() returns 8 for my machine. The min value is the number to always keep active. If you try to start a thread when the number of active threads is less than max (and greater than min) you'll see a 2 second delay.

So, for thread number 9 and so on:

Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread9");
    task = Task.Factory.StartNew(() =>
    {
        Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread9");
        while (true)
        {
            DoSomthing();
        }
    });

I expect there to be a delay of 2 seconds between

Logger.WriteInfo(LogCode.EMPTY_INFO, "before thread9");

and

 Logger.WriteInfo(LogCode.EMPTY_INFO, "after thread9");

because I try to start a thread (from thread pool) when the number of active threads is greater than min (greater than 8). The actual result is that there is just a delay of like a few little milliseconds (less than half a second) for all of thread 9 till 13.

Why isn't there a delay of 2 seconds at least? I mean, all the active threads in my thread pool are busy, and for thread 9 it is needed to allocate another thread, so there supposed to be a delay.

After writing this sample app, I'm not sure about the scenario in my previous question.

TheGeneral
  • 79,002
  • 9
  • 103
  • 141
Cod Fish
  • 917
  • 1
  • 8
  • 37
  • could you summarise what the concrete question here is? – jazb Dec 04 '18 at 08:10
  • @JohnB Why don't I get a delay when I try to start thread number 9 (from thread pool), even though all the active threads in my thread pool are busy (ThreadPool.GetMinThreads() = 8). – Cod Fish Dec 04 '18 at 08:13
  • This is actually all explained in the documentation – TheGeneral Dec 04 '18 at 08:14
  • @TheGeneral Can you please give me a link to documentation of this? Documentation of MSDN? – Cod Fish Dec 04 '18 at 08:15
  • Sorry i misread your question and jumped to a conclusion about what you want answered. So what is the actual question, why are you getting thread 9 to 13 and its not waiting 2 seconds? – TheGeneral Dec 04 '18 at 08:26
  • @TheGeneral Yes, why isn't there a delay (of more than half a second) for threads 9 to 13? – Cod Fish Dec 04 '18 at 08:29
  • 1
    it is not that simple. It used to be that the threadpool manager added a new thread twice per second. But that has been improved on in CLR v4, it now uses a feedback loop to determine an optimal number. Background in [this video](https://channel9.msdn.com/Shows/Going+Deep/Erika-Parsons-and-Eric-Eilebrecht--CLR-4-Inside-the-new-Threadpool). They didn't stop tinkering with it so beware it might be outdated as well. – Hans Passant Dec 04 '18 at 08:37

2 Answers2

2

There are a couple of problems here.

  • First is, you are using Task.Factory.StartNew which you probably shouldn't be, in most cases you should probably be using the more modern Task.Run there are oodles of questions and blogs written on this.

  • Secondly, you are quoting a question that is quoting documentation that is older then the current framework. The documentation has changed. It used to stipulate a millisecond delay in creating threads.

  • Thirdly, Tasks are not threads.

My understanding of this is the task scheduler (depending on which you use) Uses heuristics to determine whether it wants to give you a thread or not in each category, and there is no arbitrary millisecond delay.

What the documentation says currently is.

ThreadPool Class

The thread pool provides new worker threads or I/O completion threads on demand until it reaches the minimum for each category. When a minimum is reached, the thread pool can create additional threads in that category or wait until some tasks complete.

The fact is, if you are relying on a typical behavior of the task scheduler to divvy out threads at a certain pace, then you are a surely doing something wrong. This is an implementation detail and may change from version to version. At best you can raise the minimum amount of threads, but the task schedulers job is to abstract you from this level of detail to a large degree. Its designed to do the best thing for you.

If you need a certain amount of threads, either build your own task scheduler or, create your own threads and skip the task scheduler all together

TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • Regarding 'First' and the provided sample. To me it looks like he might need to be using `StartNew` icw `LongRunning`. Depending if: the amount of longrunning are reasonable, as `inlining` is not enabled (or is it nowadays?) for the top level tasks. Your insights? – Terence Feb 15 '23 at 19:02
1

Well, looking into the documentation of MSDN for the ThreadPool, I didn't find anything related to a 2 seconds delay when reaching value greater than Minimum value, and I think it is not accurate answer because the delay depends on many factors related to hardware and OS, the folloiwng is specified:

By default, the minimum number of threads is set to the number of processors on a system. When the minimum is reached, the thread pool can create additional threads in that category or wait until some tasks complete. Beginning with the .NET Framework 4, the thread pool creates and destroys threads in order to optimize throughput, which is defined as the number of tasks that complete per unit of time. Too few threads might not make optimal use of available resources, whereas too many threads could increase resource contention.

And the documentation for GetMinThreads is the following:

Retrieves the minimum number of threads the thread pool creates on demand, as new requests are made, before switching to an algorithm for managing thread creation and destruction

And the documentation states no more details about that Switching Algorithm, so what is the source of this 2 seconds delay, is it from testing on his machine ?

However, if you need more info about this the .NET framework Source is public you can check the algorithm in depth but also you won't gain a static number for the delay because of the hardware and OS dependence factors.

Ali Ezzat Odeh
  • 2,093
  • 1
  • 17
  • 17