¿Parallel.ForEach limita el número de subprocesos activos?

107

Dado este código:

var arrayStrings = new string[1000];
Parallel.ForEach<string>(arrayStrings, someString =>
{
    DoSomething(someString);
});

¿Se generarán los 1000 hilos casi simultáneamente?

Jader Dias
fuente

Respuestas:

149

No, no iniciará 1000 subprocesos; sí, limitará la cantidad de subprocesos que se utilizan. Parallel Extensions usa una cantidad adecuada de núcleos, en función de cuántos tiene físicamente y cuántos ya están ocupados. Asigna trabajo para cada núcleo y luego usa una técnica llamada robo de trabajo para permitir que cada subproceso procese su propia cola de manera eficiente y solo necesita realizar cualquier acceso costoso entre subprocesos cuando realmente lo necesita.

Eche un vistazo al Blog del equipo de PFX para obtener mucha información sobre cómo asigna el trabajo y todo tipo de otros temas.

Tenga en cuenta que, en algunos casos, también puede especificar el grado de paralelismo que desee.

Jon Skeet
fuente
2
Estaba usando Parallel.ForEach (FilePathArray, path => ... para leer alrededor de 24,000 archivos esta noche creando un nuevo archivo para cada archivo que leo. Código muy simple. Parece que incluso 6 subprocesos fueron suficientes para abrumar el disco de 7200 RPM Estaba leyendo al 100% de utilización. Durante el período de unas pocas horas, vi cómo la biblioteca Parallel giraba más de 8,000 subprocesos. Probé usando MaxDegreeOfParallelism y, efectivamente, los 8000+ subprocesos desaparecieron. Lo he probado varias veces ahora con el mismo resultar.
Jake dibujó
Se podría empezar a 1000 hilos por algún degenerado 'HacerAlgo'. (Como en el caso en el que actualmente estoy lidiando con un problema en el código de producción que no pudo establecer un límite y generó más de 200 subprocesos, lo que hizo estallar el grupo de conexiones SQL ... Recomiendo configurar el Max DOP para cualquier trabajo que no pueda ser razonado trivialmente sobre como estar explícitamente vinculado a la CPU.)
user2864740
Particionador - docs.microsoft.com/en-us/dotnet/api/…
rafidheen
28

En una máquina de un solo núcleo ... Parallel.ForEach particiona (fragmentos) de la colección en la que está trabajando entre un número de subprocesos, pero ese número se calcula en base a un algoritmo que tiene en cuenta y parece monitorear continuamente el trabajo realizado por el subprocesos que está asignando a ForEach. Entonces, si la parte del cuerpo de ForEach llama a funciones de bloqueo / enlazadas de E / S de ejecución prolongada que dejarían el hilo esperando, el algoritmo generará más hilos y reparticionará la colección entre ellos . Si los subprocesos se completan rápidamente y no se bloquean en los subprocesos IO, por ejemplo, como simplemente calcular algunos números,el algoritmo aumentará (o incluso disminuirá) el número de subprocesos hasta un punto en el que el algoritmo considere óptimo para el rendimiento (tiempo medio de finalización de cada iteración) .

Básicamente, el grupo de subprocesos detrás de todas las diversas funciones de la biblioteca Parallel funcionará con un número óptimo de subprocesos para usar. El número de núcleos de procesador físico forma solo una parte de la ecuación. NO existe una relación simple uno a uno entre el número de núcleos y el número de subprocesos generados.

No encuentro muy útil la documentación sobre la cancelación y el manejo de la sincronización de hilos. Es de esperar que MS pueda proporcionar mejores ejemplos en MSDN.

No olvide que el código del cuerpo debe escribirse para ejecutarse en varios subprocesos, junto con todas las consideraciones habituales de seguridad de subprocesos, el marco no abstrae ese factor ... todavía.

Desarrollador de Microsoft
fuente
1
"... si la parte del cuerpo de ForEach llama a funciones de bloqueo de ejecución prolongada que dejarían el hilo esperando, el algoritmo generará más hilos ..." - En casos degenerados, esto significa que puede haber tantos hilos creados como permitido por ThreadPool.
user2864740
2
Tiene razón, para IO puede asignar +100 subprocesos ya que yo mismo
depuré
5

Calcula un número óptimo de subprocesos según el número de procesadores / núcleos. No todos aparecerán a la vez.

Colin Mackay
fuente
5

Consulte ¿Utiliza Parallel.For una tarea por iteración? para tener una idea de un "modelo mental" a utilizar. Sin embargo, el autor afirma que "al final del día, es importante recordar que los detalles de implementación pueden cambiar en cualquier momento".

Kevin Hakanson
fuente
4

Gran pregunta. En su ejemplo, el nivel de paralelización es bastante bajo incluso en un procesador de cuatro núcleos, pero con un poco de espera, el nivel de paralelización puede ser bastante alto.

// Max concurrency: 5
[Test]
public void Memory_Operations()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    Parallel.ForEach<string>(arrayStrings, someString =>
    {
        monitor.Add(monitor.Count);
        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

Ahora mire lo que sucede cuando se agrega una operación en espera para simular una solicitud HTTP.

// Max concurrency: 34
[Test]
public void Waiting_Operations()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    Parallel.ForEach<string>(arrayStrings, someString =>
    {
        monitor.Add(monitor.Count);

        System.Threading.Thread.Sleep(1000);

        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

Aún no he realizado ningún cambio y el nivel de simultaneidad / paralelización ha aumentado drásticamente. La simultaneidad puede aumentar su límite con ParallelOptions.MaxDegreeOfParallelism.

// Max concurrency: 43
[Test]
public void Test()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    var options = new ParallelOptions {MaxDegreeOfParallelism = int.MaxValue};
    Parallel.ForEach<string>(arrayStrings, options, someString =>
    {
        monitor.Add(monitor.Count);

        System.Threading.Thread.Sleep(1000);

        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

// Max concurrency: 391
[Test]
public void Test()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    var options = new ParallelOptions {MaxDegreeOfParallelism = int.MaxValue};
    Parallel.ForEach<string>(arrayStrings, options, someString =>
    {
        monitor.Add(monitor.Count);

        System.Threading.Thread.Sleep(100000);

        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

Recomiendo el entorno ParallelOptions.MaxDegreeOfParallelism. No necesariamente aumentará la cantidad de subprocesos en uso, pero garantizará que solo inicie una cantidad razonable de subprocesos, lo que parece ser su preocupación.

Por último, para responder a su pregunta, no, no hará que todos los hilos comiencen a la vez. Utilice Parallel.Invoke si está buscando invocar en paralelo perfectamente, por ejemplo, probando condiciones de carrera.

// 636462943623363344
// 636462943623363344
// 636462943623363344
// 636462943623363344
// 636462943623363344
// 636462943623368346
// 636462943623368346
// 636462943623373351
// 636462943623393364
// 636462943623393364
[Test]
public void Test()
{
    ConcurrentBag<string> monitor = new ConcurrentBag<string>();
    ConcurrentBag<string> monitorOut = new ConcurrentBag<string>();
    var arrayStrings = new string[1000];
    var options = new ParallelOptions {MaxDegreeOfParallelism = int.MaxValue};
    Parallel.ForEach<string>(arrayStrings, options, someString =>
    {
        monitor.Add(DateTime.UtcNow.Ticks.ToString());
        monitor.TryTake(out string result);
        monitorOut.Add(result);
    });

    var startTimes = monitorOut.OrderBy(x => x.ToString()).ToList();
    Console.WriteLine(string.Join(Environment.NewLine, startTimes.Take(10)));
}
Timothy González
fuente