Reputation: 734
Here I have a strange behavior in my testing code, the code is quite simple, created a bunch of Timers with same pattern: add a Thread.Sleep in all the callback. Then start the timers nearly the same time, then I can see some timer's callback was delayed.
public class StrangTimerTesting
{
public int callback_EnteredTimes;
// for avoid timers get GCed.
private List<System.Timers.Timer> timerContainer = new List<System.Timers.Timer>();
public void Go()
{
for (var i = 0; i < 5; i++)
{
var displayTimer = new System.Timers.Timer(1000);
displayTimer.Elapsed += (a, b) =>
{
Interlocked.Increment(ref this.callback_EnteredTimes);
var initalTime = DateTime.Now.ToString("HH:mm:ss.ffff");
Console.WriteLine("entered times: " + callback_EnteredTimes + ", intial@" + initalTime);
displayTimer.Stop();
// why this Sleep cause some callback delayed to be called????
Thread.Sleep(6000);
};
displayTimer.Start();
timerContainer.Add(displayTimer);
}
}
}
I suppose all the callback would be called nearly the same time though on different thread pool thread, but the testing result obviously didn't support this, there're some 2 seconds gap, if i removed that Thread.Sleep, then everything is fine. could somebody point out the reason?
EDIT1: this is the result from the testing program:
*entered times: 1, intial@08:43:29.4732
entered times: 2, intial@08:43:29.4762
entered times: 3, intial@08:43:30.4763
entered times: 4, intial@08:43:30.9764
entered times: 5, intial@08:43:31.4764*
And MinThreads count in my laptop is 2.
Upvotes: 1
Views: 336
Reputation: 941337
You created a firehose problem. You have 5 timers, each ticking at 1 second, whose Elapsed event handler sleeps for 6 seconds. You are hoping that your program in effect will add 5 threads every second that need 30 seconds to get their job done. Or put another way, every minute it adds 300 threads and completes only 10 of them.
If left unchecked, this will not come to a good end. To put it mildly. A thread is a very expensive operating system resource. Beyond 5 handles, it consumes a megabyte of virtual memory. In a 32-bit process, it takes your program only 7 minutes to consume all available memory and crash with OutOfMemoryException.
.NET will not let you do this, not without putting up a fight. The counter-measure it uses is the one you observed, it does not let you start that many threads. It intentionally slows down the rate of new ones allowing to start. It will only allow 2 new threads every second on your machine.
This is the job of the ThreadPool scheduler. It attempts to keep the number of executing TP threads down to the set minimum. On your machine that minimum is 2, the number of cores you have. Having it run more than 2 doesn't make a lot of sense, the operating system will have to do more work to give those threads a chance to run, context-switching between them. In effect slowing them down, the perfect number is 2 so they have a good chance to get unrestricted access to the processor and complete their job as quickly as possible.
The ThreadPool scheduler however doesn't know what the threads are doing, it has very imperfect knowledge of the code they are executing. It doesn't know that your threads are not actually getting any work done at all and are just sleeping. It has a counter-measure for that. Twice a second, it overrides its default scheduling policy if the active threads are not completing, it allows an extra thread to start. This continues, if absolutely necessary, up to the set maximum allowed. A very high number, it should be around 500 on your machine.
This is a pretty effective algorithm, it will prevent your program from exploding after 7 minutes. It still explodes, but that will take a very, very long time. You eventually still get OOM from having many millions of TP thread start requests that the TP scheduler cannot service. Otherwise just an unreasonable outcome that you always need to expect when you write an unreasonable program.
Upvotes: 7
Reputation: 340188
From the ThreadPool docs on MSDN:
When a minimum is reached, the thread pool can create additional threads or wait until some tasks complete.
I suspect that your thread pool minimum value defaults to 2 because your system has 2 cores. The framework will add threads to the threadpool id there's work to do but all the pool threads are currently busy. However, as mention in the docs the framework might wait a bit (and apprently that's happening) speculating that a worker thread in use will possibly become available soon.
You can output the minimum threadpool count using the ThreadPool.GetMinThreads()
method.
Upvotes: 1