-1

I am working on a program where we are constantly starting new threads to go off and do a piece of work. We noticed that even though we might have started 10 threads only 3 or 4 were executing at a time. To test it out I made a basic example like this:

private void startThreads()
{
    for (int i = 0; i < 100; i++)
    {
         //Task.Run(() => someThread());
                            
         //Thread t = new Thread(() => someThread());
         //t.Start();
                            
         ThreadPool.QueueUserWorkItem(someThread);
    }
}

private void someThread()
{
    Thread.Sleep(1000);
}

Simple stuff right? Well, the code creates the 100 threads and they start to execute... but only 3 or 4 at a time. When they complete the next threads start to execute. I would have expected that almost all of them start execution at the same time. For 100 threads (each with a 1 second sleep time) it takes about 30 seconds for all of them to complete. I would have thought it would have taken far less time than this.

I have tried using Thread.Start, ThreadPool and Tasks, all give me the exact same result. If I use ThreadPool and check for the available number of threads each time a thread runs there are always >2000 available worker threads and 1000 available async threads.

I just used the above as a test for our code to try and find out what is going on. In practice, the code spawns threads all over the place. The program is running at less than 5% CPU usage but is getting really slow because the threads aren't executing quick enough.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Big Bill
  • 17
  • 1
  • `but only 3 or 4 at a time.` What _exact_ CPU do you have in your machine? – mjwills Aug 09 '21 at 23:46
  • 1
    Could you please run `ThreadPool.GetMaxThreads` withing your application method and share values that this method returns. – Botan Aug 09 '21 at 23:52
  • Is there a particular reason you are using `QueueUserWorkItem` vs `Task.Run`? https://stackoverflow.com/questions/38880743/task-run-vs-threadpool-queueuserworkitem – mjwills Aug 10 '21 at 00:00
  • 1
    Does this answer your question? [ThreadPool not starting new Thread instantly](https://stackoverflow.com/questions/7600774/threadpool-not-starting-new-thread-instantly) Specifically, Jim's [answer](https://stackoverflow.com/a/7600923/324260). Use SetMinThreads if you want to spin up the threads immediately. – Ilian Aug 10 '21 at 00:43
  • As a side note, `someThread` is a poor name for a method. [Microsoft's guidelines](https://learn.microsoft.com/en-us/previous-versions/dotnet/netframework-1.1/4df752aw(v=vs.71)): *1. Use verbs or verb phrases to name methods. 2. Use Pascal case.* `DoWork`, `ProcessItem`, `PutAThreadToSleep` are some examples of better names. – Theodor Zoulias Aug 10 '21 at 03:01
  • *"We are constantly starting new threads to go off and do a piece of work."*: The [`ThreadPool.QueueUserWorkItem`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool.queueuserworkitem) does not start new threads. What it does is schedule work on the `ThreadPool`. It's up to the `ThreadPool` to decide if and when will start new threads, or recycle old threads. If you want to start a new `Thread`, use the `Thread` constructor. – Theodor Zoulias Aug 10 '21 at 03:07
  • *"and 1000 available async threads"*: There is no such thing as an "async thread". – Theodor Zoulias Aug 10 '21 at 03:12

1 Answers1

0

Yes you may only have a few threads running at the same time. That how a ThreadPool works. It doesn't necessarily run all the threads at the same time. It would queue them up fast, but then leave it to the ThreadPool to handle when each thread runs.

If you want to ensure all 100 threads run simultaneously you can use:

ThreadPool.SetMinThreads(100, 100);

For example, see the code below, this is the result without the thread pool min size:

No MinThreads

internal void startThreads()
    {
        ThreadPool.GetMaxThreads(out int maxThread, out int completionPortThreads);

        stopwatch.Start();
        var result = Parallel.For(0, 20, (i) =>
        {
            ThreadPool.QueueUserWorkItem(someThread, i);
        });

        while (!result.IsCompleted) { }
        Console.WriteLine("Queueing completed...");
    }

    private void someThread(Object stateInfo)
    {
        int threadNum = (int)stateInfo;
        Console.WriteLine(threadNum + " started.");
        Thread.Sleep(10);
        Console.WriteLine(threadNum + " finnished.");
    }

Result (No MinThreads)

9 started.
7 started.
11 started.
10 started.
1 finnished.
12 started.
9 finnished.
13 started.
2 finnished.
4 finnished.
15 started.
3 finnished.
8 finnished.
16 started.
10 finnished.
6 finnished.
19 started.
0 finnished.
14 started.
5 finnished.
7 finnished.
17 started.
18 started.
11 finnished.

With MinThreads

internal void startThreads()
    {
        ThreadPool.GetMaxThreads(out int maxThread, out int completionPortThreads);
        ThreadPool.SetMinThreads(20, 20); // HERE <-------

        stopwatch.Start();
        var result = Parallel.For(0, 20, (i) =>
        {
            ThreadPool.QueueUserWorkItem(someThread, i);
        });

        while (!result.IsCompleted) { }
        Console.WriteLine("Queueing completed...");
    }

Results

...
7 started.
15 started.
9 started.
12 started.
17 started.
13 started.
16 started.
19 started.
18 started.
5 finnished.
3 finnished.
4 finnished.
6 finnished.
0 finnished.
14 finnished.
1 finnished.
10 finnished.
...

A nice clean devise.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
CorrieJanse
  • 2,374
  • 1
  • 6
  • 23
  • 1
    What's the reason for scheduling work on the `ThreadPool` by using a parallel loop? The OP used a simple `for` loop to schedule the work. Do you think that your approach has some advantage? – Theodor Zoulias Aug 10 '21 at 03:03
  • @TheodorZoulias, it ques them faster. In the context of the program it has no advantage, but rather it was to put emphasis that even though the Tasks are queued at the same time, they don't run at the same time. – CorrieJanse Aug 10 '21 at 23:20
  • I just did a rough performance test. Scheduling 1,000,000 `WaitCallback`s with the `ThreadPool.QueueUserWorkItem` using a simple `for` loop takes ~300 milliseconds in my machine. Doing the same with a `Parallel.For` loop takes roughly the same time (slightly more). I don't think that this operation is parallelizable honestly. Probably it takes a `lock` internally, to ensure that the operation is thread-safe, serializing the calls anyway. – Theodor Zoulias Aug 11 '21 at 03:37