Estoy tratando de usar la biblioteca Parallel para mi código y me enfrento a un problema extraño. Hice un programa corto para demostrar el comportamiento. En resumen, hago 2 bucles (uno dentro de otro). El primer ciclo genera una matriz aleatoria de 200 enteros y el segundo ciclo agrega todas las matrices en una lista grande. El problema es que, al final, no obtengo un múltiplo de 200 enteros, sino que veo que algunas ejecuciones no esperan a que la matriz aleatoria se cargue por completo. Es difícil de explicar, así que aquí el código de muestra:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace TestParallel { class Program { static int RecommendedDegreesOfParallelism = 8; static int DefaultMaxPageSize = 200; static void Main(string[] args) { int maxPage = 50; List<int> lstData = new List<int>(); Parallel.For(0, RecommendedDegreesOfParallelism, new ParallelOptions() { MaxDegreeOfParallelism = RecommendedDegreesOfParallelism }, (index) => { int cptItems = 0; int cptPage = 1 - RecommendedDegreesOfParallelism + index; int idx = index; do { cptPage += RecommendedDegreesOfParallelism; if (cptPage > maxPage) break; int Min = 0; int Max = 20; Random randNum = new Random(); int[] test2 = Enumerable .Repeat(0, DefaultMaxPageSize) .Select(i => randNum.Next(Min, Max)) .ToArray(); var lstItems = new List<int>(); lstItems.AddRange(test2); var lstRes = new List<int>(); lstItems.AsParallel().WithDegreeOfParallelism(8).ForAll((item) => { lstRes.Add(item); }); Console.WriteLine($"{Task.CurrentId} = {lstRes.Count}"); lstData.AddRange(lstRes); cptItems = lstRes.Count; } while (cptItems == DefaultMaxPageSize); } ); Console.WriteLine($"END: {lstData.Count}"); Console.ReadKey(); } } }Y aquí hay un registro de ejecución:
4 = 200 1 = 200 2 = 200 3 = 200 6 = 200 5 = 200 7 = 200 8 = 200 1 = 200 6 = 194 2 = 191 5 = 200 7 = 200 8 = 200 4 = 200 5 = 200 3 = 182 4 = 176 8 = 150 7 = 200 5 = 147 1 = 200 7 = 189 1 = 200 1 = 198 END: 4827
Podemos ver que algunos bucles devuelven menos de 200 elementos. ¿Como es posible?
Esto aquí no es seguro para subprocesos:
lstItems.AsParallel().WithDegreeOfParallelism(8).ForAll((item) => { lstRes.Add(item); }); De la documentación para List<T> :
Es seguro realizar varias operaciones de lectura en una lista, pero pueden surgir problemas si se modifica la colección mientras se lee. Para garantizar la seguridad de los subprocesos, bloquee la colección durante una operación de lectura o escritura. Para habilitar el acceso a una colección por múltiples subprocesos para lectura y escritura, debe implementar su propia sincronización.
No lo menciona explícitamente, pero .Add() también puede fallar cuando varios subprocesos lo llaman simultáneamente.
La solución sería bloquear las llamadas a List<T>.Add() en el ciclo anterior, pero si lo hace, es probable que sea más lento que simplemente agregar los elementos en un ciclo en un solo hilo.
var locker = new object(); lstItems.AsParallel().WithDegreeOfParallelism(8).ForAll((item) => { lock (locker) { lstRes.Add(item); } });