2011-02-11 11 views
9

Estoy haciendo un proyecto que genera cientos de subprocesos. Todos estos subprocesos están en una condición "inactiva" (están bloqueados en un objeto Monitor). Me he dado cuenta de que si aumente el número de hilos "dormidos", el programa se ralentizará mucho. Lo "gracioso" es que al mirar al Administrador de tareas parece que cuanto mayor es el número de subprocesos, más libre es el procesador. Reduje el problema a la creación de objetos.Desaceleración en la creación de objetos con muchos subprocesos

¿Alguien me puede explicar esto?

He producido una pequeña muestra para probarlo. Es un programa de consola. Crea un hilo para cada procesador y mide su velocidad con una prueba simple (un "nuevo Objeto()"). No, el "nuevo Objeto()" no está jodido (prueba si no confías en mí). El hilo principal muestra la velocidad de cada hilo. Al presionar CTRL-C, el programa engendra 50 hilos "dormidos". La ralentización comienza con solo 50 hilos. Con alrededor de 250, es muy visible en el Administrador de tareas que la CPU no se usa al 100% (en la mía es 82%).

He intentado tres métodos para bloquear el hilo "dormido": Thread.CurrentThread.Suspend() (malo, malo, lo sé :-)), un bloqueo en un objeto ya bloqueado y un Thread.Sleep (Timeout .Infinito). Es lo mismo. Si comento la fila con el nuevo Object(), y la reemplazo con Math.Sqrt (o sin nada), el problema no está presente. La velocidad no cambia con la cantidad de hilos. ¿Alguien más puede verificarlo? ¿Alguien sabe dónde está el cuello de la botella?

Ah ... debe probarlo en modo de lanzamiento SIN iniciarlo desde Visual Studio. Estoy usando XP sp3 en un procesador dual (sin HT). Lo he probado con .NET 3.5 y 4.0 (para probar los diferentes tiempos de ejecución marco)

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

Si le preocupa el rendimiento, no debería tener muchas más cadenas (de cuenta). Entre (cpucount + 2) y (cpucount * 2) son buenas reglas generales (y en tu sistema, ambas salen a 4). Use colas de operaciones de E/S asincrónicas para mantener ocupados los pocos hilos en lugar de dormir. La única vez que un hilo debe esperar es cuando se disputa un bloqueo. –

+0

Estoy haciendo corutinas "a cámara lenta". El "tiempo de cambio" entre hilos es irrelevante, así que puedo usar hilos (tengo un "interruptor"/segundo, así que aunque pierda algunos ms para hacer el cambio entre el hilo viejo y el hilo nuevo, no tengo algún problema). Siempre hay una cantidad de subprocesos igual al procesador. Pero si los subprocesos ralentizan todo, entonces tengo un problema. No, no puedo usar la biblioteca asíncrona de MS, porque es "falso". "Reescribe" tu programa. Tengo que usar algunas bibliotecas preexistentes. – xanatos

+0

¿Ha considerado utilizar el TPL en lugar de crear hilos explícitamente? De esta forma, el marco puede decidir la cantidad más apropiada de hilos nativos para hacer el trabajo. –

Respuesta

5

Mi suposición es que el problema es que la recolección de basura requiere un cierto grado de cooperación entre las roscas - ya sea algo necesidades para comprobar que todos están suspendidas, o pedirles que suspender a sí mismos y esperar a que ocurra, etc. (e incluso si son suspendida, tiene que decirles que no se despierta!)

Este describe un recolector de basura "stop the world", por supuesto. Creo que hay al menos dos o tres implementaciones diferentes de GC que difieren en los detalles sobre el paralelismo ... pero sospecho que todos van a tener algo de que hacer en términos de hacer que los hilos cooperen.

+0

He intentado con el "servidor" GC. Se asigna un GC y un montón para cada procesador. La aplicación escala mejor Con 100 Threads pierde "solo" un 10% de velocidad en la asignación de objetos. – xanatos

+0

Cuantas más pruebas hago, más estoy convencido de que es el GC. Es muy difícil "comparar" el GC y distinguir su tiempo del tiempo para la creación de los objetos, pero al final esto no cambia nada de mi POV: muchos subprocesos = objetos "nuevos" lentos (al menos porque nuevos) objetos causan GC Collection). Servidor GC = Bueno cuando muchos hilos. Podría intentar agrupar objetos, pero creo que aumentaría la complejidad ... Ya lo veré. ¡Gracias! – xanatos

10

Inicie Taskmgr.exe, pestaña Procesos. Ver + Seleccionar columnas, marcar "Falla de página Delta". Verás el impacto de la asignación de cientos de megabytes, solo para almacenar las acumulaciones de todos estos temas que creaste. Cada vez que ese número parpadea para su proceso, el programa bloquea la espera de la paginación del sistema operativo en los datos del disco en la RAM.

TANSTAAFL, No existe tal cosa como un almuerzo gratis.

+0

Espacio de pila de modo de usuario de 1 MB más otro espacio de pila de modo nativo de 1 MB, el tamaño predeterminado para cada subproceso en la creación. –

+2

@Chris, no hay una pila de modo nativo, una pila sirve ambos. Sin embargo, cada subproceso creado también tiene una pila de modo kernel de 24 KB. –

+0

@Hans, gracias por la aclaración, quise decir kernel en lugar de native, pero pensé que este tamaño también era de 1MB. –

1

Lo que está viendo aquí es el GC en acción. Al adjuntar un depurador a su proceso se verá que muchas excepciones de forma

Unknown exception - code e0434f4e (first chance) 

se tiran. Estas son excepciones causadas por el GC para reanudar un hilo suspendido. Como usted sabe, no se recomienda llamar a Suspend/ResumeThread dentro de su proceso. Esto es aún más cierto en el mundo administrado. La única autoridad que puede hacer esto de manera segura es el GC. Cuando se establece un punto de interrupción en SuspendThread verá

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]) 

que el GC tratar de suspender todas sus hilos antes de que pueda hacer una colección completa. En mi máquina (32 bit, Windows 7, .NET 3.5 SP1) la desaceleración no es tan dramática. Veo una dependencia lineal entre el conteo de hilos y el uso de la CPU (no). Parece que ve un aumento en los costos para cada GC porque el GC tiene que suspender más subprocesos antes de que pueda hacer una recolección completa. Curiosamente, el tiempo se gasta principalmente en modo de usuario, por lo que el kernel no es el factor limitante.

No veo una forma de cómo evitarlo, excepto usar menos hilos o usar código no administrado. Podría ser que si aloja el CLR usted mismo y usa fibras en lugar de hilos físicos, el GC escalará mucho mejor. Lamentablemente, esta característica fue cut out durante el ciclo de actualización de .NET 2.0. Como ahora son 6 años después, hay pocas esperanzas de que se agregue nuevamente.

Además de su número de hilos, el GC también se ve limitado por la complejidad de su gráfico de objetos. Eche un vistazo a este "Do You Know The Costs Of Garbage?".

+0

+1 Sí, descubrí que era el GC el que creaba el problema . Probablemente intente suspender incluso los hilos que ya están esperando algo, por lo que es O (n) con n = número total de hilos en lugar de O (m) con m = el número de hilos en ejecución. Tristemente ya había investigado el truco de Fiber y sabía que estaba cortado :-(Y el CTP Async más viejo tenía algunos problemas donde era lento ejecutar una Tarea que terminaba inmediatamente sin esperar otra cosa (deberían haberlo resuelto con el Async CTP más nuevo pero mientras tanto comencé a trabajar en otro proyecto) – xanatos

+0

Creo que la razón por la que no puede ser O (m) es que si espera, por ejemplo, con un tiempo de espera, puede que algunos subprocesos se activen en medio de un GC. Además, podría despertar un hilo desde el cual el GC cree que está suspendido mientras supera todos los hilos. –

+0

Lo podrían haber resuelto de otras maneras. Los diversos Wait no necesitan comunicarse directamente con el sistema operativo. podría haber sido "mediado" por el CG. Eligieron hacerlo de esta manera, tenemos que trabajar con él. – xanatos

Cuestiones relacionadas