Parallel.ForEach vs Task.Factory.StartNew


267

Qual è la differenza tra i frammenti di codice seguenti? Non userete entrambi i thread di threadpool?

Ad esempio, se desidero chiamare una funzione per ciascun elemento in una raccolta,

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}

Risposte:


302

La prima è un'opzione molto migliore.

Parallel.ForEach, internamente, usa a Partitioner<T>per distribuire la tua collezione in oggetti di lavoro. Non eseguirà un'attività per articolo, ma piuttosto raggruppa questo per ridurre i costi generali coinvolti.

La seconda opzione pianificherà un singolo Taskper articolo nella tua raccolta. Mentre i risultati saranno (quasi) gli stessi, questo introdurrà un sovraccarico molto maggiore del necessario, specialmente per le grandi collezioni, e rallenterà i tempi complessivi di funzionamento.

Cordiali saluti - Il partizionatore utilizzato può essere controllato usando i sovraccarichi appropriati su Parallel.ForOach , se lo si desidera. Per i dettagli, consultare Partizionatori personalizzati su MSDN.

La differenza principale, in fase di esecuzione, è che la seconda agirà in modo asincrono. Questo può essere duplicato usando Parallel.ForEach facendo:

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

In questo modo, si sfrutta comunque i partizionatori, ma non si blocca fino al completamento dell'operazione.


8
IIRC, il partizionamento predefinito eseguito da Parallel.ForEach tiene conto anche del numero di thread hardware disponibili, evitando di dover elaborare il numero ottimale di attività da avviare. Consulta l'articolo Patterns of Parallel Programming di Microsoft ; ha grandi spiegazioni di tutte queste cose al suo interno.
Mal Ross,

2
@Mal: sorta di ... In realtà non è il partizionatore, ma piuttosto il lavoro del TaskScheduler. TaskScheduler, per impostazione predefinita, utilizza il nuovo ThreadPool, che ora gestisce molto bene.
Reed Copsey,

Grazie. Sapevo che avrei dovuto lasciare l'avvertenza "Non sono un esperto, ma ...". :)
Mal Ross,

@ReedCopsey: come collegare le attività avviate tramite Parallel.PerEach all'attività wrapper? In modo che quando si chiama .Wait () su un'attività wrapper si blocca fino al completamento delle attività in esecuzione in parallelo?
Konstantin Tarkus,

1
@Tarkus Se stai effettuando più richieste, stai meglio usando HttpClient.GetString in ogni elemento di lavoro (nel tuo ciclo parallelo). Nessun motivo per inserire un'opzione asincrona all'interno del loop già concorrente, in genere ...
Reed Copsey,

89

Ho fatto un piccolo esperimento per eseguire un metodo "1.000.000.000 (un miliardo)" volte con "Parallel.For" e uno con oggetti "Task".

Ho misurato il tempo del processore e ho trovato Parallel più efficiente. Parallelo: per suddividere l'attività in piccoli oggetti di lavoro ed eseguirli su tutti i nuclei in parallelo in modo ottimale. Durante la creazione di molti oggetti task (FYI TPL utilizzerà internamente il pool di thread), ogni esecuzione su ciascuna attività verrà spostata creando più stress nella casella, come risulta dall'esperimento di seguito.

Ho anche creato un piccolo video che spiega il TPL di base e ha anche dimostrato come Parallel.For utilizza il tuo core in modo più efficiente http://www.youtube.com/watch?v=No7QqSc5cl8 rispetto alle normali attività e discussioni.

Esperimento 1

Parallel.For(0, 1000000000, x => Method1());

Esperimento 2

for (int i = 0; i < 1000000000; i++)
{
    Task o = new Task(Method1);
    o.Start();
}

Confronto dei tempi del processore


Sarebbe più efficiente e la ragione per cui la creazione di thread è costosa L'esperimento 2 è una cattiva pratica.
Tim

@ Georgi, ti prego di parlare di più su ciò che è male.
Shivprasad Koirala,

3
Mi dispiace, errore mio, avrei dovuto chiarire. Intendo la creazione di Task in un ciclo a 1000000000. Il sovraccarico è inimmaginabile. Per non parlare del fatto che Parallel non può creare più di 63 attività alla volta, il che lo rende molto più ottimizzato nel caso.
Georgi-it,

Questo è vero per le attività 1000000000. Tuttavia, quando elaboro un'immagine (ripetutamente, ingrandendo il frattale) e eseguo Parallelo. Per le linee molti core sono inattivi mentre aspettano che finiscano gli ultimi thread. Per renderlo più veloce ho suddiviso i dati da me in 64 pacchetti di lavoro e creato attività per esso. (Quindi Task.WaitAll attende il completamento.) L'idea è di fare in modo che i thread inattivi raccolgano un pacchetto di lavoro per aiutare a completare il lavoro invece di attendere 1-2 thread per completare il loro blocco (Parallel.For) assegnato.
Tedd Hansen,

1
Cosa fa Mehthod1()in questo esempio?
Zapnologica,

17

Parallel.ForEach ottimizzerà (potrebbe anche non avviare nuovi thread) e si bloccherà fino al termine del ciclo, mentre Task.Factory creerà esplicitamente una nuova istanza di attività per ciascun elemento e tornerà prima che siano terminati (attività asincrone). Parallel.Foreach è molto più efficiente.


11

A mio avviso, lo scenario più realistico è quando le attività hanno un'operazione pesante da completare. L'approccio di Shivprasad si concentra più sulla creazione di oggetti / allocazione della memoria che sul calcolo stesso. Ho fatto una ricerca chiamando il seguente metodo:

public static double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result; 
}

L'esecuzione di questo metodo richiede circa 0,5 secondi.

L'ho chiamato 200 volte usando Parallel:

Parallel.For(0, 200, (int i) =>
{
    SumRootN(10);
});

Poi l'ho chiamato 200 volte usando il vecchio stile:

List<Task> tasks = new List<Task>() ;
for (int i = 0; i < loopCounter; i++)
{
    Task t = new Task(() => SumRootN(10));
    t.Start();
    tasks.Add(t);
}

Task.WaitAll(tasks.ToArray()); 

Primo caso completato in 26656 ms, il secondo in 24478 ms. L'ho ripetuto molte volte. Ogni volta che il secondo approccio è marginalmente più veloce.


L'uso di Parallel.For è alla vecchia maniera. L'uso di Task è consigliato per le unità di lavoro che non sono uniformi. Gli MVP di Microsoft e i progettisti del TPL menzionano anche che l'uso delle attività utilizzerà i thread in modo più efficiente, non bloccandone altrettanti in attesa del completamento di altre unità.
Suncat 2000,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.