Parallel.Foreach forma de desove demasiadas hebras
-
18-09-2019 - |
Pregunta
El problema
A pesar de que el código de la que voy a hablar aquí he escrito en C #, que se basa en el marco .NET 4, que no depende específicamente en ninguna particularidad de F # (al menos eso parece!).
Tengo algunas piezas de datos en mi disco que debería actualizar desde la red, ahorrando la versión más reciente en el disco:
type MyData =
{ field1 : int;
field2 : float }
type MyDataGroup =
{ Data : MyData[];
Id : int }
// load : int -> MyDataGroup
let load dataId =
let data = ... // reads from disk
{ Data = data;
Id = dataId }
// update : MyDataGroup -> MyDataGroup
let update dg =
let newData = ... // reads from the network and process
// newData : MyData[]
{ dg with Data = dg.Data
|> Seq.ofArray
|> Seq.append newData
|> processDataSomehow
|> Seq.toArray }
// save : MyDataGroup -> unit
let save dg = ... // writes to the disk
let loadAndSaveAndUpdate = load >> update >> save
El problema es que a loadAndSaveAndUpdate
todos mis datos, que tendría que ejecutar la función muchos veces:
{1 .. 5000} |> loadAndSaveAndUpdate
Cada paso haría
- alguna S de disco,
- algún crujido de datos,
- alguna red IO (con posibilidad de un montón de latencia),
- más datos crujido,
- y algunos S de disco.
¿No sería bueno tener este hecho en paralelo, hasta cierto punto? Por desgracia, ninguno de mis funciones de lectura y de análisis son "asíncrono flujos de trabajo listo".
Los primeros (no muy bueno) soluciones que se le ocurrió
Tareas
Lo primero que he hecho fue la creación de un Task[]
y empezar a todos ellos:
let createTask id = new Task(fun _ -> loadAndUpdateAndSave id)
let tasks = {1 .. 5000}
|> Seq.map createTask
|> Seq.toArray
tasks |> Array.iter (fun x -> x.Start())
Task.WaitAll(tasks)
Entonces me golpeó CTRL + ESC sólo para ver cuántos hilos que estaba utilizando. 15, 17, ..., 35, ..., 170, ... hasta que mataron a la aplicación! Algo iba mal.
paralelo
Lo hice casi lo mismo pero utilizando Parallel.ForEach(...)
y los resultados fueron los mismos:. Montones y montones y montones de hilos
Una solución que funciona ... tipo de
Entonces decidí comenzar sólo hilos n
, Task.WaitAll(of them)
, a continuación, otra n
, hasta que no hubo más tareas disponibles.
Esto funciona, pero el problema es que cuando se ha terminado de procesar, digamos, tareas n-1
, se va a esperar, esperar, esperar a que la maldita última tarea que insisten en el bloqueo debido a la gran cantidad de latencia de la red. Esto no es bueno!
Por lo tanto, ¿cómo atacar este problema ? Apreciaría para ver diferentes soluciones, la que se utilizan los flujos de trabajo asincrónicos (y en este caso la forma de adaptar mis funciones no asincrónicos), extensiones paralelas, paralelas patrones extraños, etc.
Gracias.
Solución
¿Está seguro de que sus tareas individuales están terminando en el momento oportuno? Creo que tanto Parallel.ForEach
y la clase Task
ya usan el .NET subprocesos. Las tareas deben ser generalmente elementos de trabajo de corta duración, en cuyo caso el threadpool sólo generar un pequeño número de hilos reales, pero si sus tareas no están haciendo progresos y hay otras tareas en cola, entonces el número de hilos utilizados, se incrementará hasta el máximo (que por defecto es 250 / procesador en .NET 2.0 SP1, pero es diferente en diferentes versiones del marco). Es también digno de mención que (al menos en .NET 2.0 SP1) de nueva creación del hilo es estrangulado a 2 nuevos temas por segundo, por lo que llegar hasta el número de hilos que estamos viendo indica que las tareas no están terminando en un corto período de tiempo (por lo que puede que no sea completamente exacta de echar la culpa a Parallel.ForEach
).
Creo que la sugerencia de Brian utilizar flujos de trabajo async
es buena, sobre todo si la fuente de las tareas de larga vida es IO, ya async
volverá sus hilos a la subprocesos hasta que el IO completa. Otra opción es aceptar simplemente que sus tareas no están completando rápidamente y permitir que el desove de muchas discusiones (que puede ser controlada hasta cierto punto mediante el uso de System.Threading.ThreadPool.SetMaxThreads
) - dependiendo de su situación puede que no sea un gran problema que está utilizando una gran cantidad de hilos.
Otros consejos
ParallelOptions.MaxDegreeOfParallelism límites el número de operaciones simultáneas a cargo de las llamadas método paralelo
El uso de 'asincrónicos le permitirá hacer el I / O-ligado trabajo sin quemar hilos, mientras que las diversas llamadas de E / S son 'en el mar', por lo que sería mi primera sugerencia. Debe ser sencillo para convertir el código para asíncrono, por lo general a lo largo de las líneas de
- envolver cada cuerpo de la función en
async{...}
, añadirreturn
cuando sea necesario - crear versiones asíncronas de cualquier primitivas de E / S que no están ya en la biblioteca a través de
Async.FromBeginEnd
- llamadas cambia de forma
let r = Foo()
alet! r = AsyncFoo()
- Uso
Async.Parallel
para convertir los objetos asincrónicos 5000 en un solo asíncrono que se ejecuta en paralelo
Hay varios tutoriales para hacer esto; una tal transmisión es rel="noreferrer"> .
Siempre se puede utilizar un ThreadPool
.
http://msdn.microsoft.com/en -us / biblioteca / system.threading.threadpool.aspx
básicamente:
- Crear un grupo de subprocesos
- Establecer el número máximo de hilos
- Cola de todas las tareas utilizando
QueueUserWorkItem(WaitCallback)