You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Even though it resembles the later, it manifests differently.
The ThreadPool Thread Count remains very low at a constant value of 5-6 and never increases.
The ThreadPool Queue Length oscillates between 6-30.
The TimerCallback is called once and then it "stalls", even though an expectation is that it should be called every second.
ThreadPool Completed Work Item Count (Count / 1 sec) 68
ThreadPool Queue Length 18
ThreadPool Thread Count 6
Time spent in JIT (ms / 1 sec) 0
0:013> !sos.threadpool
logStart: 0
logSize: 7
CPU utilization: 31 Worker Thread: Total: 5 Running: 5 Idle: 0 MaxLimit: 32767 MinLimit: 4
Work Request in Queue: 0
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread:Total: 1 Free: 1 MaxFree: 8 CurrentLimit: 1 MaxLimit: 1000 MinLimit: 4
privatestaticvoidTimerCallback(objectstate)=>Console.WriteLine(state?.ToString()+DateTime.UtcNow);privatestaticvoidEndlessLoopProcessor(){while(true){varts=Enumerable.Range(0,6).Select(_ =>Task.Run(()=>Thread.Sleep(16))).ToArray();Task.WaitAll(ts);WaitInsideEndlessLoopProcessor();}}privatestaticvoidWaitInsideEndlessLoopProcessor()=>Thread.Sleep(100);privatestaticasyncTaskAwaitEverything(){while(true){awaitTask.Delay(1000);// <-- almost never get pass this awaitTimerCallback("AwaitEverything: ");}}staticasyncTaskMain(string[]args){vartimer=newTimer(TimerCallback,"main: ",0,1000);varmain=Task.Run(async()=>{vartasks=Enumerable.Range(0,10).Select(_ =>Task.Run(async()=>EndlessLoopProcessor())).ToArray();Console.WriteLine(DateTime.UtcNow);Console.WriteLine("-----------------");awaitAwaitEverything();});Console.WriteLine("Press enter to end.");Console.ReadLine();awaitmain;GC.KeepAlive(timer);}
Questions
Based on the referenced articles, I assumed/verified that EndlessLoopProcessor keeps local thread-pool queues full.
What I don't understand is why the timer callback is never executed?
If callbacks in the global queue are never processed due to all running worker threads are busy processing
their local queues, why thread-pool simply doesn't spawn a new worker thread?
If my assumption regarding timer callback being queued into the global queue is correct, it's even more puzzling, because when I inspected the global queue I wasn't able to find the timer callback there (I might just missed it though).
When I attach debugger and freeze/un-freeze threads executing EndlessLoopProcessor there is a high chance the program "un-stalls" and continues as would be expected, that is, the TimerCallback gets called every second.
When this happens !sos.threadpool shows that number of worker threads is higher than it used to be.
That might explain why the timer callbacks resume, because now there is a worker thread which can process the timer callback.
Why doesn't the thread-pool increase the number of worker threads itself?
In the following scenarios, the thread-pool does increase the number of worker threads.
Just to be clear, I'm not interested in how to solve this situation, because it's obvious - don't block the thread pool thread as EndlessLoopProcessor does (using a dedicated thread for EndlessLoopProcessor does the trick, either creating it manually or using Task.Factory.StartNew with TaskCreationOptions.LongRunning hint which essentially does the same).
I'm curious, why the thread-pool doesn't self-tune by increasing the number of worker threads?
I observed this behavior on .NET Framework 4.8, .NET Core 3.1 and .NET 6.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
Description
I ran into a thread-pool/timer "starvation" situation I haven't seen before.
It doesn't seem to be the case of neither debug-threadpool-starvation nor another case from Kevin Gosse.
Even though it resembles the later, it manifests differently.
The ThreadPool Thread Count remains very low at a constant value of 5-6 and never increases.
The ThreadPool Queue Length oscillates between 6-30.
The TimerCallback is called once and then it "stalls", even though an expectation is that it should be called every second.
The minimal code to reproduce the starvation
Questions
Based on the referenced articles, I assumed/verified that EndlessLoopProcessor keeps local thread-pool queues full.
What I don't understand is why the timer callback is never executed?
If callbacks in the global queue are never processed due to all running worker threads are busy processing
their local queues, why thread-pool simply doesn't spawn a new worker thread?
If my assumption regarding timer callback being queued into the global queue is correct, it's even more puzzling, because when I inspected the global queue I wasn't able to find the timer callback there (I might just missed it though).
When I attach debugger and freeze/un-freeze threads executing EndlessLoopProcessor there is a high chance the program "un-stalls" and continues as would be expected, that is, the TimerCallback gets called every second.
When this happens
!sos.threadpool
shows that number of worker threads is higher than it used to be.That might explain why the timer callbacks resume, because now there is a worker thread which can process the timer callback.
Why doesn't the thread-pool increase the number of worker threads itself?
In the following scenarios, the thread-pool does increase the number of worker threads.
Remarks
Just to be clear, I'm not interested in how to solve this situation, because it's obvious - don't block the thread pool thread as EndlessLoopProcessor does (using a dedicated thread for EndlessLoopProcessor does the trick, either creating it manually or using
Task.Factory.StartNew
withTaskCreationOptions.LongRunning
hint which essentially does the same).I'm curious, why the thread-pool doesn't self-tune by increasing the number of worker threads?
I observed this behavior on .NET Framework 4.8, .NET Core 3.1 and .NET 6.
Beta Was this translation helpful? Give feedback.
All reactions