【问题标题】:Slowdown on creating objects with many threads使用多线程创建对象的速度减慢
【发布时间】:2011-06-25 14:13:39
【问题描述】:

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

谁能给我解释一下?

我已经制作了一个小样本来测试它。这是一个控制台程序。它为每个处理器创建一个线程,并通过一个简单的测试(“new Object()”)测量它的速度。不,“new Object()”并没有被忽略(如果你不信任我,请尝试)。主线程显示每个线程的速度。按下 CTRL-C,程序生成 50 个“休眠”线程。减速开始时只有 50 个线程。大约 250 时,在任务管理器上很明显 CPU 没有 100% 使用(我的使用率为 82%)。

我尝试了三种锁定“睡眠”线程的方法:Thread.CurrentThread.Suspend()(不好,不好,我知道 :-)),锁定已经锁定的对象和 Thread.Sleep(Timeout.无限的)。一样的。如果我用 new 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);
                }
            }
        }
    }
}

【问题讨论】:

  • 如果您关心性能,您的线程数不应超过 (cpucount) 个。 (cpucount+2) 和 (cpucount*2) 之间是很好的经验法则(在您的系统上,两者都是 4)。使用异步 I/O 操作队列使少数线程保持忙碌而不是休眠。线程应该等待的唯一时间是在争夺锁时。
  • 我正在做一个“慢动作”协程。线程之间的“切换时间”是无关紧要的,所以我可以使用线程(我有大约一个“切换”/秒,所以即使它失去了一些毫秒来在旧线程和新线程之间切换我没有任何问题)。总是有许多线程运行等于处理器但是如果睡眠线程减慢一切,那么我就有问题了。不,我不能使用 MS 的异步库,因为它是“假的”。它“重写”你的程序。我必须使用一些预先存在的库。
  • 您是否考虑过使用 TPL 而不是显式创建线程?这样,框架可以决定最合适的本地线程数来完成这项工作。
  • @Richard 我的问题是我想要协程,而 .NET 没有实现它们。我正在使用多个线程模拟它们。我什至尝试过使用异步 CTP,但预刷新速度很慢。我必须在刷新后重试。

标签: c# .net multithreading performance asynchronous


【解决方案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?"

【讨论】:

  • +1 是的,我发现是 GC 造成了问题。他可能会尝试暂停已经在等待某事的线程,所以它是 O(n),n = 线程总数,而不是 O(m),m = 正在运行的线程数。可悲的是,我已经研究过 Fiber 技巧,我知道它已被删除 :-( 并且较旧的 Async CTP 存在一些问题,即运行任务很慢,该任务立即终止而无需等待其他东西(他们应该用较新的异步 CTP,但同时我开始从事另一个项目)
  • 我认为它不能是 O(m) 的原因是如果你等待例如超时,您的某些线程可能会在 GC 中间唤醒。除此之外,您还可以唤醒一个 GC 认为它已暂停的线程,而它确实暂停了所有线程。
  • 他们本可以通过其他方式解决它。各种等待不需要直接与操作系统通信。他们的重启可能是由GC“调解”的。他们选择这样做,我们必须与之合作。
【解决方案2】:

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

TANSTAAFL,天下没有免费的午餐。

【讨论】:

  • 1 MB 用户模式堆栈空间加上另外 1 MB 本机模式堆栈空间,创建时每个线程的默认大小。
  • @Chris,没有本机模式堆栈,一个堆栈同时服务。但是,创建的每个线程也有一个 24 KB 的内核模式堆栈。
  • @Hans,感谢您的澄清,我的意思是内核代替本机,但我认为这个大小也是 1MB。
  • 我正在处理 256kb 堆栈的线程。内存没有真正分配,只有虚拟空间。增量稳定在 32 左右。100 线程 * 256kb = 25mb。我不是在 C64 上测试它,你知道吗? :-) 我有 3gb 的内存。
  • “第 32 轮稳定”不好,只有 0 好。 CLR 提交整个堆栈,它不仅像在本机代码中那样被保留。查看列出的其他进程,不同之处在于它们不会创建数百个线程。
【解决方案3】:

我的猜测是问题在于垃圾收集需要线程之间进行一定程度的合作 - 要么需要检查它们是否都已挂起,要么要求它们自行挂起并等待它会发生,等等。(即使他们暂停,它也必须告诉他们不要醒来!)

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

【讨论】:

  • 我已经尝试过“服务器”GC。它为每个处理器分配一个 GC 和一个堆。该应用程序可以更好地扩展。使用 100 个线程时,它会“仅”损失 10% 的对象分配速度。
  • 我做的测试越多,我就越确信它是 GC。很难对 GC 进行“基准测试”并将其时间与创建对象的时间区分开来,但最终这并没有改变我的 POV:许多线程 = 缓慢的“新”对象(至少因为 new对象导致 GC 收集)。服务器 GC = 多线程时很好。我可以尝试对象池,但我认为它会增加复杂性......我会看到的。谢谢!
猜你喜欢
  • 1970-01-01
  • 2015-04-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多