减慢创建具有许multithreading的对象

我正在做一个产生数百个线程的项目。 所有这些线程都处于“hibernate”状态(它们被锁定在Monitor对象上)。 我注意到,如果我增加“hibernate”线程的数量,程序会非常慢。 “有趣”的是,看着任务管理器似乎线程数越多,处理器就越自由。 我已将问题缩小到对象创建。

有人可以向我解释一下吗?

我制作了一个小样本来测试它。 这是一个控制台程序。 它为每个处理器创建一个线程,并通过简单的测试(“新对象()”)测量它的速度。 不,“新的对象()”没有被淘汰(如果你不信任我,试试)。 主线程显示每个线程的速度。 按CTRL-C,该程序产生50个“睡眠”线程。 减速开始只有50个线程。 在任务管理器中,大约250个非常明显,CPU不是100%使用的(我的是82%)。

我已经尝试了三种锁定“hibernate”线程的方法:Thread.CurrentThread.Suspend()(坏,坏,我知道:-)),锁定已经锁定的对象和Thread.Sleep(Timeout.Infinite)。 一样的。 如果我使用新的Object()注释该行,并将其替换为Math.Sqrt(或没有任何内容),则问题不存在。 速度不随线程数而变化。 别人可以查一下吗? 有谁知道瓶颈在哪里?

啊……你应该在发布模式下测试它,不要从Visual Studio中启动它。 我在双处理器上使用XP sp3(没有HT)。 我用.NET 3.5和4.0测试了它(测试不同的框架运行时)

namespace TestSpeed { using System; using System.Collections.Generic; using System.Threading; class Program { private const long ticksInSec = 10000000; private const long ticksInMs = ticksInSec / 1000; private const int threadsTime = 50; private const int stackSizeBytes = 256 * 1024; private const int waitTimeMs = 1000; private static List collects = new List(); private static int[] objsCreated; static void Main(string[] args) { objsCreated = new int[Environment.ProcessorCount]; Monitor.Enter(objsCreated); for (int i = 0; i  { if (e.SpecialKey != ConsoleSpecialKey.ControlC) { return; } for (int i = 0; i  { /* The same for all the three "ways" to lock forever a thread */ //Thread.CurrentThread.Suspend(); //Thread.Sleep(Timeout.Infinite); lock (objsCreated) { } }, stackSizeBytes).Start(); Interlocked.Increment(ref numThreads); } e.Cancel = true; }; while (true) { Thread.Sleep(waitTimeMs); Console.SetCursorPosition(0, 1); DateTime now = DateTime.UtcNow; long ticks = (now - last).Ticks; Console.WriteLine("Slept for {0}ms", ticks / ticksInMs); Thread.MemoryBarrier(); for (int i = 0; i < objsCreated.Length; i++) { int count = objsCreated[i]; Console.WriteLine("{0} [{1} Threads]: {2}/sec ", i, numThreads, ((long)(count - oldCount[i])) * ticksInSec / ticks); oldCount[i] = count; } Console.WriteLine(); CheckCollects(); last = now; } } private static void Worker(object obj) { int ix = (int)obj; while (true) { /* First and second are slowed by threads, third, fourth, fifth and "nothing" aren't*/ new Object(); //if (new Object().Equals(null)) return; //Math.Sqrt(objsCreated[ix]); //if (Math.Sqrt(objsCreated[ix])  collects.Count) { collects.Add(0); } for (int i = 0; i < collects.Count; i++) { int newCol = GC.CollectionCount(i); if (newCol != collects[i]) { collects[i] = newCol; Console.WriteLine("Collect gen {0}: {1}", i, newCol); } } } } } 

我的猜测是问题是垃圾收集需要线程之间的一定程度的合作 – 要么需要检查它们是否全部被暂停,要么让它们暂停并等待它发生等等。(即使它们暂停了,它必须告诉它们不要醒来!)

当然,这描述了一个“停止世界”的垃圾收集器。 我相信至少有两三种不同的GC实现在并行性方面的细节上有所不同……但我怀疑所有这些实现在获得线程合作方面都有一些工作要做。

启动Taskmgr.exe,进程选项卡。 查看+选择列,勾选“Page Fault Delta”。 您将看到分配数百兆字节的影响,只是为了存储您创建的所有这些线程的堆栈。 每当该进程的数字闪烁时,程序就会阻塞操作系统将数据从磁盘分页到RAM。

TANSTAAFL,没有免费午餐这样的东西。

你在这里看到的是GC在行动。 将调试器附加到进程时,您将看到表单的许多exception

 Unknown exception - code e0434f4e (first chance) 

被扔了。 这是由GC恢复挂起的线程引起的exception。 如您所知,强烈建议您不要在进程中调用Suspend / ResumeThread。 在托管世界中更是如此。 可以安全地执行此操作的唯一权限是GC。 在SuspendThread设置断点时,您将看到

 0118f010 5f3674da 00000000 00000000 83e36f53 KERNEL32!SuspendThread 0118f064 5f28c51d 00000000 83e36e63 00000000 mscorwks!Thread::SysSuspendForGC+0x2b0 (FPO: [Non-Fpo]) 0118f154 5f28a83d 00000001 00000000 00000000 mscorwks!WKS::GCHeap::SuspendEE+0x194 (FPO: [Non-Fpo]) 0118f17c 5f28c78c 00000000 00000000 0000000c mscorwks!WKS::GCHeap::GarbageCollectGeneration+0x136 (FPO: [Non-Fpo]) 0118f208 5f28a0d3 002a43b0 0000000c 00000000 mscorwks!WKS::gc_heap::try_allocate_more_space+0x15a (FPO: [Non-Fpo]) 0118f21c 5f28a16e 002a43b0 0000000c 00000000 mscorwks!WKS::gc_heap::allocate_more_space+0x11 (FPO: [Non-Fpo]) 0118f23c 5f202341 002a43b0 0000000c 00000000 mscorwks!WKS::GCHeap::Alloc+0x3b (FPO: [Non-Fpo]) 0118f258 5f209721 0000000c 00000000 00000000 mscorwks!Alloc+0x60 (FPO: [Non-Fpo]) 0118f298 5f2097e6 5e2d078c 83e36c0b 00000000 mscorwks!FastAllocateObject+0x38 (FPO: [Non-Fpo]) 

GC确实会尝试暂停所有线程,然后才能进行完整收集。 在我的机器上(32位,Windows 7,.NET 3.5 SP1),减速并不那么引人注目。 我确实看到线程计数和CPU(非)使用之间存在线性依赖关系。 您似乎看到每个GC的成本增加,因为GC必须暂停更multithreading才能进行完全收集。 有趣的是,时间主要用在用户模式上,因此内核不是限制因素。

除了使用更少的线程或使用非托管代码之外,我确实在网上看到了如何解决这个问题。 可能是因为如果您自己托管CLR并使用Fibers而不是物理线程,那么GC将会更好地扩展。 不幸的是,这个function在.NET 2.0的重新安装周期中被删除了 。 自从现在6年后,人们几乎没有希望它会再次被添加。

除了线程数之外,GC还受到对象图复杂性的限制。 看看这个“你知道垃圾的成本吗?” 。