Замедление при создании объектов с множеством потоков - PullRequest
9 голосов
/ 11 февраля 2011

Я делаю проект, который порождает несколько сотен потоков.Все эти потоки находятся в «спящем» состоянии (они заблокированы на объекте Monitor).Я заметил, что если я увеличу количество «спящих» потоков, программа сильно замедлится.Самое смешное, что, глядя на диспетчер задач, кажется, что чем больше потоков, тем свободнее процессор.Я сузил проблему до создания объекта.

Может кто-нибудь объяснить это мне?

Я изготовил небольшой образец для его проверки.Это консольная программа.Он создает поток для каждого процессора и измеряет его скорость с помощью простого теста («новый объект ()»).Нет, «новый объект ()» не исчезает (попробуйте, если вы мне не доверяете).Основной поток показывает скорость каждого потока.При нажатии CTRL-C программа порождает 50 «спящих» потоков.Замедление начинается с 50 потоков.Примерно с 250 видно, что в диспетчере задач процессор не используется на 100% (у меня это 82%).

Я пробовал три способа блокировки «спящего» потока: 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<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 ]

10 голосов
/ 11 февраля 2011

Запустите Taskmgr.exe, вкладка Процессы.Просмотр + Выбор столбцов, отметьте «Page Fault Delta».Вы увидите влияние распределения сотен мегабайт только для хранения стеков всех созданных вами потоков.Каждый раз, когда число мигает для вашего процесса, ваша программа блокирует ожидание подкачки операционной системой данных с диска в ОЗУ.

TANSTAAFL, бесплатного обеда не бывает.

5 голосов
/ 11 февраля 2011

My думаю, в том, что проблема в том, что сборка мусора требует определенного взаимодействия между потоками - что-то нужно либо проверить, что все они приостановлены, либо попросить их приостановить себя и дождаться этого случиться и т. д. (И даже если они приостановлены, он должен сказать им не просыпаться!)

Конечно, здесь описывается сборщик мусора "останови мир". Я полагаю, что есть по крайней мере две или три разные реализации GC, которые отличаются деталями вокруг параллелизма ... но я подозреваю, что у всех них будет некоторая работа, чтобы заставить потоки взаимодействовать .

1 голос
/ 10 сентября 2011

То, что вы видите здесь, это 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) замедление не так сильно. Я вижу линейную зависимость между количеством потоков и использованием процессора (не). Кажется, вы видите увеличение затрат на каждый GC, потому что GC должен приостановить большее количество потоков, прежде чем он сможет выполнить полный сбор. Интересно, что время тратится в основном на пользовательский режим, поэтому ядро ​​не является ограничивающим фактором.

Я действительно вижу способ, как вы могли бы обойти это, за исключением использования меньшего количества потоков или неуправляемого кода. Может случиться так, что если вы самостоятельно разместите CLR и используете Fibers вместо физических потоков, GC будет масштабироваться намного лучше. К сожалению, эта функция была отключена во время цикла обращения .NET 2.0. Поскольку прошло уже 6 лет, мало надежды на то, что он будет добавлен снова.

Кроме того, из вашего числа потоков GC также ограничен сложностью вашего графа объектов. Взгляните на это «Знаете ли вы стоимость мусора?» .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...