c# - programming - Parallel.Foreach+rendimiento rendimiento?
parallel programming in c# (3)
No quiero ser ofensivo, pero tal vez haya una falta de comprensión. Parallel.ForEach
significa que el TPL ejecutará el foreach según el hardware disponible en varios subprocesos. ¡Pero eso significa, que es posible hacer ese trabajo en paralelo! yield return
le da la oportunidad de obtener algunos valores de una lista (o lo que sea) y devolverlos uno por uno a medida que sean necesarios. Evita la necesidad de encontrar primero todos los elementos que coincidan con la condición y luego iterar sobre ellos. De hecho, es una ventaja de rendimiento, pero no se puede hacer en paralelo.
Quiero procesar algo usando un bucle paralelo como este:
public void FillLogs(IEnumerable<IComputer> computers)
{
Parallel.ForEach(computers, cpt=>
{
cpt.Logs = cpt.GetRawLogs().ToList();
});
}
Ok, funciona bien. Pero, ¿cómo hacerlo si quiero que el método FillLogs devuelva un IEnumerable?
public IEnumerable<IComputer> FillLogs(IEnumerable<IComputer> computers)
{
Parallel.ForEach(computers, cpt=>
{
cpt.Logs = cpt.GetRawLogs().ToList();
yield return cpt // KO, don''t work
});
}
EDITAR
Parece que no es posible ... pero uso algo como esto:
public IEnumerable<IComputer> FillLogs(IEnumerable<IComputer> computers)
{
return computers.AsParallel().Select(cpt => cpt);
}
Pero donde puse el cpt.Logs = cpt.GetRawLogs().ToList();
instrucción
Qué tal si
Queue<string> qu = new Queue<string>();
bool finished = false;
Task.Factory.StartNew(() =>
{
Parallel.ForEach(get_list(), (item) =>
{
string itemToReturn = heavyWorkOnItem(item);
lock (qu)
qu.Enqueue(itemToReturn );
});
finished = true;
});
while (!finished)
{
lock (qu)
while (qu.Count > 0)
yield return qu.Dequeue();
//maybe a thread sleep here?
}
Edit: creo que esto es mejor:
public static IEnumerable<TOutput> ParallelYieldReturn<TSource, TOutput>(this IEnumerable<TSource> source, Func<TSource, TOutput> func)
{
ConcurrentQueue<TOutput> qu = new ConcurrentQueue<TOutput>();
bool finished = false;
AutoResetEvent re = new AutoResetEvent(false);
Task.Factory.StartNew(() =>
{
Parallel.ForEach(source, (item) =>
{
qu.Enqueue(func(item));
re.Set();
});
finished = true;
re.Set();
});
while (!finished)
{
re.WaitOne();
while (qu.Count > 0)
{
TOutput res;
if (qu.TryDequeue(out res))
yield return res;
}
}
}
Edit2: Estoy de acuerdo con el corto No hay respuesta. Este código es inútil; no se puede romper el bucle de rendimiento.
Versión corta: no, eso no es posible a través de un bloque iterador; la versión más larga probablemente implique una cola / salida en cola sincronizada entre el subproceso del iterador de la persona que llama (haciendo la salida de la cola) y los trabajadores paralelos (haciendo la puesta en cola); pero como nota al margen, los registros suelen estar enlazados a IO, y paralelizar las cosas que están enlazadas a IO a menudo no funciona muy bien.
Si la persona que llama va a tardar un tiempo en consumir cada una, entonces puede haber algún mérito en un enfoque que solo procesa un registro a la vez, pero puede hacerlo mientras la persona que llama consume el registro anterior; es decir, comienza una Task
para el siguiente elemento antes del yield
, y espera que se complete después del yield
... pero eso es, nuevamente, bastante complejo. Como ejemplo simplificado:
static void Main()
{
foreach(string s in Get())
{
Console.WriteLine(s);
}
}
static IEnumerable<string> Get() {
var source = new[] {1, 2, 3, 4, 5};
Task<string> outstandingItem = null;
Func<object, string> transform = x => ProcessItem((int) x);
foreach(var item in source)
{
var tmp = outstandingItem;
// note: passed in as "state", not captured, so not a foreach/capture bug
outstandingItem = new Task<string>(transform, item);
outstandingItem.Start();
if (tmp != null) yield return tmp.Result;
}
if (outstandingItem != null) yield return outstandingItem.Result;
}
static string ProcessItem(int i)
{
return i.ToString();
}