2011-02-11 145 views
9

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

有人可以解释给我吗?

我制作了一个小样本来测试它。这是一个控制台程序。它为每个处理器创建一个线程,并通过一个简单的测试(一个“新的Object()”)来测量它的速度。不,“新的对象()”不会被打散(尝试如果你不信任我)。主线程显示每个线程的速度。按CTRL-C,程序会产生50个“睡眠”线程。减速开始只有50个线程。在250左右,在任务管理器中非常明显,CPU不是100%使用(在我的情况下,它是82%)。我已经尝试了三种方法来锁定“睡眠”线程:Thread.CurrentThread.Suspend()(坏的,坏的,我知道:-)),对已经锁定的对象和Thread.Sleep(超时。无穷)。一样的。如果我用新的Object()注释该行,并用Math.Sqrt替换它(或没有),则问题不存在。速度不随线程数量而改变。 其他人可以检查吗?有谁知道瓶颈在哪里?

啊......你应该在发布模式下测试它,而不需要从Visual Studio启动它。 我在双处理器(无HT)上使用XP sp3。我与.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); 
       } 
      } 
     } 
    } 
} 
+3

如果您关注性能,则不应该有比(cpucount)更多的线程。 (cpucount + 2)和(cpucount * 2)之间是很好的经验法则(并且在你的系统上,都是4)。使用异步I/O操作队列来保持少量线程繁忙而不是睡眠。线程应该等待的唯一时间是在争用锁时。 – 2011-02-11 14:23:08

+0

我正在做一个“慢动作”协同程序。线程之间的“切换时间”是不相关的,所以我可以使用线程(我有一个“开关”/秒,所以即使它失去了一些毫秒,使旧线程和新线程之间切换,我没有任何问题)。总是有许多线程与处理器运行相同但是如果睡眠线程放慢了一切,那么我有一个问题。不,我不能使用MS的异步库,因为它是“假的”。它“重写”你的程序。我必须使用一些预先存在的库。 – xanatos 2011-02-11 14:49:33

+0

您是否考虑过使用TPL而不是显式创建线程?这样框架可以决定最合适的原生线程数量来完成这项工作。 – 2011-09-10 10:11:42

回答

5

猜测是,问题是,垃圾回收需要线程之间的合作一定量的 - 这要么需要检查他们是否全部暂停,或要求他们暂停自己并等待它发生等等(并且即使他们暂停,也要告诉他们不要起床!)

这个当然,描述了一个“停止世界”垃圾回收器。我相信至少有两种或三种不同的GC实现,它们在并行性方面的细节上有所不同......但我怀疑他们都会通过的某些工作来获得线程的合作。

10

启动Taskmgr.exe,进程选项卡。查看+选择列,勾选“页面错误增量”。您将看到分配数百兆字节的影响,仅用于存储您创建的所有这些线程的堆栈。每当这个数字出现在你的进程中时,你的程序就会阻塞等待操作系统从磁盘分页到RAM中。

TANSTAAFL,有没有免费的午餐这样的事情。

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?"