c# out-of-memory task-parallel-library large-data

c# - Parallel.ForEach puede causar una excepción de "Memoria insuficiente" si se trabaja con un enumerable con un objeto grande



out-of-memory task-parallel-library (3)

Estoy tratando de migrar una base de datos donde las imágenes se almacenan en la base de datos a un registro en la base de datos apuntando a un archivo en el disco duro. Estaba intentando usar Parallel.ForEach para acelerar el proceso utilizando este método para consultar los datos.

Sin embargo, noté que estaba obteniendo una excepción de OutOfMemory . Sé que Parallel.ForEach consultará un lote de enumerables para mitigar el costo de los gastos generales si hay uno para espaciar las consultas (por lo que es más probable que su fuente tenga el siguiente registro en la memoria si hace un montón de consultas a la vez) de espaciarlos). El problema se debe a que uno de los registros que estoy devolviendo es una matriz de bytes de 1-4 Mb que hace que el espacio de direcciones completo se agote (El programa debe ejecutarse en modo x86 ya que la plataforma objetivo será una máquina de 32 bits )

¿Hay alguna forma de desactivar el almacenamiento en caché o make es más pequeño para el TPL?

Aquí hay un programa de ejemplo para mostrar el problema. Esto debe cumplirse en modo x86 para mostrar el problema, si tarda mucho o no está sucediendo en su máquina aumente el tamaño de la matriz (encontré 1 << 20 tarda aproximadamente 30 segundos en mi máquina y 4 << 20 fue casi instantáneo)

class Program { static void Main(string[] args) { Parallel.ForEach(CreateData(), (data) => { data[0] = 1; }); } static IEnumerable<byte[]> CreateData() { while (true) { yield return new byte[1 << 20]; //1Mb array } } }


Entonces, aunque lo que Rick ha sugerido es definitivamente un punto importante, otra cosa que creo que falta es la discusión de la partitioning .

Parallel::ForEach utilizará una implementación Partitioner<T> predeterminada que, para un IEnumerable<T> que no tenga una longitud conocida, usará una estrategia de partición de fragmentos. Lo que esto significa es que cada hebra de trabajo que Parallel::ForEach usará para trabajar en el conjunto de datos leerá una cantidad de elementos de IEnumerable<T> que luego solo será procesada por ese hilo (ignorando el robo de trabajo por ahora) . Hace esto para ahorrar el gasto de tener que volver constantemente a la fuente y asignar un nuevo trabajo y programarlo para otro hilo de trabajo. Por lo general, esto es algo bueno. Sin embargo, en su escenario específico, imagine que está en un núcleo cuádruple y ha establecido ParallelOptions.MaximumDegreeOfParallelism en 4 hilos para su trabajo y ahora cada uno de ellos extrae un trozo de 100 elementos de su IEnumerable<T> . Bueno, eso es 100-400 megas justo allí para ese hilo de trabajo en particular, ¿verdad?

Entonces, ¿cómo resuelves esto? Fácil, escribes una implementación Partitioner<T> . Ahora, la fragmentación sigue siendo útil en su caso, por lo que probablemente no quiera ir con una estrategia de partición de elemento único porque luego se introduciría una sobrecarga con toda la coordinación de tareas necesaria para eso. En su lugar, escribiría una versión configurable que puede sintonizar mediante una aplicación hasta que encuentre el equilibrio óptimo para su carga de trabajo. La buena noticia es que, si bien escribir una implementación de este tipo es bastante directo, en realidad no tienes que escribirlo tú mismo porque el equipo de PFX ya lo hizo y ponerlo en el proyecto de muestras de programación paralela .


Este problema tiene mucho que ver con los particionadores, no con el grado de paralelismo. La solución es implementar un particionador de datos personalizado.

Si el conjunto de datos es grande, parece que se garantiza que la implementación mono del TPL se quede sin memoria. Esto me sucedió recientemente (esencialmente estaba ejecutando el ciclo anterior, y encontré que la memoria aumentaba linealmente hasta que me dio una excepción OOM )

Después de rastrear el problema, descubrí que, de forma predeterminada, mono dividirá el enumerador utilizando una clase EnumerablePartitioner. Esta clase tiene un comportamiento en que cada vez que da datos a una tarea, "fragmenta" los datos por un factor cada vez mayor (e inmutable) de 2. Por lo tanto, la primera vez que una tarea solicita datos obtiene un pedazo de tamaño 1, la próxima vez de tamaño 2 * 1 = 2, la próxima vez 2 * 2 = 4, luego 2 * 4 = 8, etc. El resultado es que la cantidad de datos entregados a la tarea, y por lo tanto almacenados en memoria simultáneamente, aumenta con la longitud de la tarea, y si se procesan muchos datos, inevitablemente ocurre una excepción de falta de memoria.

Presumiblemente, la razón original de este comportamiento es que quiere evitar que cada thread regrese varias veces para obtener datos, pero parece basarse en la suposición de que todos los datos que se procesan podrían caber en la memoria (no es el caso al leer desde archivos grandes).

Este problema se puede evitar con un particionador personalizado como se indicó anteriormente. Un ejemplo genérico de uno que simplemente devuelve los datos a cada tarea, un elemento a la vez, está aquí:

https://gist.github.com/evolvedmicrobe/7997971

Simplemente crea una instancia de esa clase primero y entrégala a Paralelo.Para en lugar del enumerable mismo


Las opciones predeterminadas para Parallel.ForEach solo funcionan bien cuando la tarea está vinculada a la CPU y se escala linealmente . Cuando la tarea está vinculada a la CPU, todo funciona perfectamente. Si tiene un núcleo cuádruple y no hay otros procesos en ejecución, entonces Parallel.ForEach usa los cuatro procesadores. Si tienes un quad-core y algún otro proceso en tu computadora usa una CPU completa, entonces Parallel.ForEach usa aproximadamente tres procesadores.

Pero si la tarea no está unida a la CPU, entonces Parallel.ForEach mantiene las tareas de inicio, intentando mantener todas las CPU ocupadas. Sin embargo, no importa cuántas tareas se ejecuten en paralelo, siempre hay más caballos de fuerza de la CPU sin usar y, por lo tanto, sigue creando tareas.

¿Cómo puedes saber si tu tarea está ligada a la CPU? Con suerte, solo inspeccionándolo. Si está factorizando números primos, es obvio. Pero otros casos no son tan obvios. La forma empírica de saber si su tarea está vinculada a la CPU es limitar el grado máximo de paralelismo con ParallelOptions.MaximumDegreeOfParallelism y observar cómo se comporta su programa. Si su tarea está unida a la CPU, debería ver un patrón como este en un sistema de cuatro núcleos:

  • ParallelOptions.MaximumDegreeOfParallelism = 1 : utilice una CPU completa o un 25% de utilización de la CPU
  • ParallelOptions.MaximumDegreeOfParallelism = 2 : utilice dos CPU o 50% de utilización de la CPU
  • ParallelOptions.MaximumDegreeOfParallelism = 4 : utilice todas las CPU o el 100% de utilización de la CPU

Si se comporta de esta manera, puede usar las opciones Parallel.ForEach predeterminadas y obtener buenos resultados. La utilización de CPU lineal significa una buena programación de tareas.

Pero si ejecuto la aplicación de muestra en mi Intel i7, obtengo un 20% de utilización de la CPU sin importar el grado máximo de paralelismo que establezca. ¿Por qué es esto? Se está asignando tanta memoria que el recolector de basura está bloqueando los hilos. La aplicación está vinculada a recursos y el recurso es memoria.

Del mismo modo, una tarea vinculada a E / S que realiza consultas de larga ejecución en un servidor de base de datos tampoco podrá utilizar todos los recursos de CPU disponibles en la computadora local. Y en casos como ese, el planificador de tareas no puede "saber cuándo detenerse" al comenzar nuevas tareas.

Si su tarea no está unida a la CPU o la utilización de la CPU no se escala linealmente con el grado máximo de paralelismo, entonces debe aconsejar a Parallel.ForEach no inicie demasiadas tareas a la vez. La forma más simple es especificar un número que permita cierto paralelismo para superponer tareas de E / S, pero no tanto como para abrumar la demanda de recursos de la computadora local o sobrecargar cualquier servidor remoto. Prueba y error está involucrado para obtener los mejores resultados:

static void Main(string[] args) { Parallel.ForEach(CreateData(), new ParallelOptions { MaxDegreeOfParallelism = 4 }, (data) => { data[0] = 1; }); }