Parallel Programming – PLINQ #3

Nelle precedenti puntate avevamo visto come elaborare in parallelo query PLINQ incrementando le prestazioni delle operazioni che andavamo ad eseguire; ma è anche possibile elaborare in parallelo  l'output proveniente  da una query parallelizzata in modo efficiente ?

Ma che domande ! Big Smile Certo che si !!! Smile

Riprendendo gli esempi utilizzati nei post precedenti,  possiamo notare come l’output proveniente dalla query parallelizzata fosse stato elaborato in modo sequenziale attraverso l’utilizzo del costrutto foreach :


var addrs = new[] { "10.1.0.2", "10.1.0.10", "10.1.0.5", "10.1.0.4", "10.1.0.3", "10.1.0.1",  

                             "10.1.0.8", "10.1.0.7", "10.1.0.9", "10.1.0.6" };           

var pings = from ip in addrs.AsParallel()
                 select new Ping().Send(ip);

foreach (var ping in pings)
{
   Console.WriteLine("{0}: {1}", ping.Status, ping.Address);
}

Ma come possiamo elaborare in parallelo l'output proveniente  da una query parallelizzata ? Potremmo andare a “scomodare” il costrutto Parallel.ForEach, che sarà approfondito in seguito in altri post. Il seguente segmento di codice riporta l’esempio precedente rivisto dal punto di vista dell’elaborazione in parallelo delll'output proveniente  da una query parallelizzata :


var addrs = new[] { "10.1.0.2", "10.1.0.10", "10.1.0.5", "10.1.0.4", "10.1.0.3", "10.1.0.1",  

                             "10.1.0.8", "10.1.0.7", "10.1.0.9", "10.1.0.6" };           

var pings = from ip in addrs.AsParallel()
                 select new Ping().Send(ip);

Parallel.ForEach(pings, ping =>
                                                 {                                       
                                                   Console.WriteLine("{0}: {1}", ping.Status, ping.Address);
                                                 }
                         );

eseguendo l’esempio ci accorgiamo subito che c’è qualcosa che non va ! Clamorosamente l’esecuzione dell’esempio sulla mia macchina dual core risulta di 2 secondi più lenta rispetto al tempo di esecuzione del precedente esempio. Ma la seconda parte dell’operazione non è stata eseguita in parallelo ? e per tanto non doveva risultare più performante ?

Il problema è proprio questo. Nel precedente post avevamo parlato della modalità di funzionamento di PLINQ :

Quando il .NET frame work esegue una query PLINQ, il data source oggetto dell’elaborazione viene partizionato utilizzando i thread disponibili nel sistema, ovvero i dati contenuti nel data source di origine vengono segmentati su ogni singolo thread per elaborare in parallelo. Man mano che l’elaborazione delle singole partizioni viene completata, i risultati confluiscono nell’oggetto di ritorno  IEnumerable<T> disponibile solo dopo che tutte le elaborazioni sono terminate.

In questo caso specifico il .NET frame work parallelizza l’elaborazione della query  partizionando i dati, quando l’elaborazione di tutte le partizioni è completata rende disponibile l’oggetto IEnumerable<T>, a questo punto cerca di elaborare in parallelo l'output proveniente  dalla query. Quindi l’istruzione Parallel.ForEach partiziona nuovamente i dati, segmentandoli per eseguire il ciclo in parallelo. Ecco spiegato il motivo per cui questo tipo di approccio risulta poco efficiente producendo un collo di bottiglia invece di apportare un beneficio in termini di performance.

Ma allora come possiamo agire per ovviare a questa problematica ? Il .Net frame work 4.0 introduce la classe ParallelQuery che rappresenta una sequenza parallela ed espone il metodo di estensione ForAll. Tale metodo richiama in parallelo l'azione specificata per ogni elemento in source.

Proviamo a riscrivere l’esempio nel seguente modo :

var addrs = new[] { "10.1.0.2", "10.1.0.10", "10.1.0.5", "10.1.0.4", "10.1.0.3", "10.1.0.1",  

                             "10.1.0.8", "10.1.0.7", "10.1.0.9", "10.1.0.6" };           

var pings = from ip in addrs.AsParallel()
                 select new Ping().Send(ip);

pings.ForAll(ping =>
                                {
                                   Console.WriteLine("{0}: {1}", ping.Status, ping.Address);
                                }
                        );

L’utilizzo di questo approccio consente di elaborare in modo efficiente l’output di una query parallelizzata poiché non richiede un passaggio di unione alla fine permettendo così di poter processare l’output di tutte le partizioni in parallelo.  Difatti eseguendo l’esempio sulla mia macchina dual core il tempo di esecuzione è inferiore a quello della versione sequenziale proposta nel primo esempio.

Per chi volesse cimentarsi con gli esempi utilizzati in questo post e magari verificare le relative tempistiche può trovarli qui.

Published Sunday, June 20, 2010 8:40 PM by leo.alario
Powered by Community Server (Commercial Edition), by Telligent Systems