这里我的测试代码中有一个奇怪的行为,代码非常简单,创建了一堆具有相同模式的Timers:在所有回调中添加 Thread.Sleep 。然后几乎同时启动计时器,然后我可以看到一些计时器的回调是延迟。
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);
}
}
}
我想虽然在不同的线程池线程上几乎同时调用了所有的回调,但测试结果显然不支持这个,有一些 2秒的差距,如果我删除了Thread.Sleep,然后一切都很好。有人可以指出原因吗?
EDIT1:这是测试程序的结果:
*输入次数:1,intial @ 08:43:29.4732
进入时间:2,初始@ 08:43:29.4762
进入时间:3,intial @ 08:43:30.4763
输入次数:4,intial @ 08:43:30.9764
输入次数:5,intial @ 08:43:31.4764 *
我的笔记本电脑中的MinThreads数量是2。
答案 0 :(得分:7)
您创建了 firehose问题。你有5个计时器,每个计时器在1秒钟内滴答,其Elapsed事件处理程序休眠6秒。您希望您的程序实际上每秒添加5个线程,需要30秒才能完成工作。换句话说,它每分钟增加300个线程并只完成其中的10个。
如果不加以控制,这将无法达到目的。说得客气一点。线程是一种非常昂贵的操作系统资源。超过5个句柄,它消耗一兆字节的虚拟内存。在32位进程中,只需7分钟即可占用所有可用内存,并在OutOfMemoryException时崩溃。
.NET不会让你这么做,不是没有打架。它使用的对策是您观察到的,它不让您启动那么多线程。它故意减慢允许启动的新速率。它只会在你的机器上每秒允许2个新线程。
这是ThreadPool调度程序的工作。它尝试将执行TP线程的数量保持在设定的最小值。在您的计算机上,最小值为2,即您拥有的核心数。让它运行超过2并没有多大意义,操作系统将不得不做更多的工作来为这些线程提供运行机会,在它们之间进行上下文切换。实际上减慢它们的速度,完美的数字是2,所以他们很有可能无限制地访问处理器并尽快完成工作。
然而,ThreadPool调度程序不知道线程正在做什么,它对它们正在执行的代码有非常不完全的了解。它不知道你的线程实际上根本没有完成任何工作而只是在睡觉。它有一个对策。如果活动线程没有完成,它会覆盖其默认调度策略,它允许额外线程启动。如果绝对必要,这将继续,直到允许的设定最大值。一个非常高的数字,你的机器应该是500左右。这是一个非常有效的算法,它会阻止您的程序在7分钟后爆炸。它仍然会爆炸,但这需要非常长的时间。你最终仍然会让OOM拥有TP调度程序无法提供服务的数百万个TP线程启动请求。否则只是一个不合理的结果,当你写一个不合理的程序时,你总是需要它。
答案 1 :(得分:1)
当达到最小值时,线程池可以创建其他线程或等到某些任务完成。
我怀疑您的线程池最小值默认为2,因为您的系统有2个核心。该框架将向线程池ID添加线程,但是所有池线程当前都很忙。但是,正如在文档中提到的那样,框架可能会等待(并且显然正在发生)推测正在使用的工作线程很快就会可用。
您可以使用ThreadPool.GetMinThreads()
方法输出最小线程池数。