我正在做一个产生数百个线程的项目。所有这些线程都处于“休眠”状态(它们被锁定在Monitor对象上)。我注意到,如果我增加“休眠”线程的数量,程序会非常慢。 “有趣”的是,看着任务管理器似乎线程数越多,处理器就越自由。我已将问题缩小到对象创建。
有人可以向我解释一下吗?
我制作了一个小样本来测试它。这是一个控制台程序。它为每个处理器创建一个线程,并通过简单的测试(“新对象()”)测量它的速度。不,“新对象()”没有被淘汰(如果你不信任我,试试)。主线程显示每个线程的速度。按CTRL-C,该程序产生50个“睡眠”线程。减速开始于50个线程。在任务管理器上可以看到大约250个,CPU不是100%使用(在我的是82%)。
我已经尝试了三种锁定“休眠”线程的方法:Thread.CurrentThread.Suspend()(坏,坏,我知道:-)),锁定已经锁定的对象和Thread.Sleep(超时。无穷)。一样的。如果我使用新的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<int> collects = new List<int>();
private static int[] objsCreated;
static void Main(string[] args)
{
objsCreated = new int[Environment.ProcessorCount];
Monitor.Enter(objsCreated);
for (int i = 0; i < objsCreated.Length; i++)
{
new Thread(Worker).Start(i);
}
int[] oldCount = new int[objsCreated.Length];
DateTime last = DateTime.UtcNow;
Console.Clear();
int numThreads = 0;
Console.WriteLine("Press Ctrl-C to generate {0} sleeping threads, Ctrl-Break to end.", threadsTime);
Console.CancelKeyPress += (sender, e) =>
{
if (e.SpecialKey != ConsoleSpecialKey.ControlC)
{
return;
}
for (int i = 0; i < threadsTime; i++)
{
new Thread(() =>
{
/* 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]) < 0) return;
//Interlocked.Add(ref objsCreated[ix], 0);
Interlocked.Increment(ref objsCreated[ix]);
}
}
private static void CheckCollects()
{
int newMax = GC.MaxGeneration;
while (newMax > 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);
}
}
}
}
}
答案 0 :(得分:10)
启动Taskmgr.exe,进程选项卡。查看+选择列,勾选“Page Fault Delta”。您将看到分配数百兆字节的影响,只是为了存储您创建的所有这些线程的堆栈。每当该进程的数字闪烁时,程序就会阻塞操作系统将数据从磁盘分页到RAM中。
TANSTAAFL,没有免费午餐这样的东西。
答案 1 :(得分:5)
我的猜测问题是垃圾收集需要线程之间进行一定程度的合作 - 要么需要检查它们是否全部暂停,要么让它们暂停并等待它会发生等等(即使它们 暂停,它也必须告诉它们不要醒来!)
这当然描述了一个“停止世界”的垃圾收集器。我相信至少有两三种不同的GC实现在并行性方面的细节上有所不同......但是我怀疑所有这些实现都会让一些在获取线程方面做些工作。合作。
答案 2 :(得分:1)
你在这里看到的是GC在行动。将调试器附加到流程时,您将看到表单的许多例外
Unknown exception - code e0434f4e (first chance)
被抛出。这是由GC恢复挂起的线程引起的异常。如您所知,强烈建议您不要在进程中调用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必须先挂起更多线程才能进行完全收集。有趣的是,时间主要用在用户模式上,因此内核不是限制因素。
我确实看到了一种方法,除了使用更少的线程或使用非托管代码之外,你可以解决这个问题。如果您自己托管CLR并使用Fibers而不是物理线程,GC可能会更好地扩展。不幸的是,在.NET 2.0的重新循环期间,此功能是cut out。由于它现在已经过去了6年,因此很少有希望再次添加它。
除了你的线程计数之外,GC还受到对象图复杂性的限制。看看这个"Do You Know The Costs Of Garbage?"。