venerdì 15 aprile 2011

Parallelismo .Net

Con l'avvento del .NET Framework 3.5, arriva anche la tanto attesa preview pubblica delle Parallel Extensions for .NET Framework, una libreria managed per gestire il parallelismo di task e dati su hardware parallelo, con un work scheduler comune, liberando il programmatore dalla gestione dei dettagli inerenti la concorrenza. Oltre al download del package completo di binari ed esempi, è disponibile anche quello della sola documentazione.

La necessità del .NET Framework 3.5 è legata alla presenza di Parallel Language Integrated Query (PLINQ), il paradigma dichiarativo di codice parallelo che permette allo sviluppatore di esprimere cosa vuole ottenere piuttosto che come, direttamente derivato dalle caratteristiche uniche al mondo di LINQ. PLINQ viene esposto dal namespace System.Linq contenuto nell'assembly System.Threading.dll, che andrà referenziato in ogni progetto.

Alle funzionalità di PLINQ si accede in due modi. Il primo è tramite la classe System.Linq.ParallelEnumerable, che espone un'implementazione concorrente dei medesimi metodi esposti nel core da un'enumeration LINQ. Per fissare le idee, una query su una collection System.Linq.Enumerable viene effettuata, in LINQ, dal seguente codice:
  1. <span style="font-size:1.0em">
  2.       IEnumerable data = ...;
  3.       var q = Enumerable.Select(
  4.                   Enumerable.OrderBy(
  5.                       Enumerable.Where(data, x => p(x)),
  6.                       x => k(x)),
  7.                   x => f(x));
  8.       foreach (var e in q) a(e);
  9. </span>
La potenza espressiva di questa sintassi è già di per sé formidabile, e dà una pallida idea degli inarrivabili vertici cui è giunto il framework .NET. Ma la meraviglia delle meraviglie è la sintassi con cui la medesima query può essere eseguita in maniera concorrente:
  1. <span style="font-size:1.0em">
  2.       IEnumerable data = ...;
  3.       var q = ParallelEnumerable.Select(
  4.                   ParallelEnumerable.OrderBy(
  5.                       ParallelEnumerable.Where(data, x => p(x)),
  6.                       x => k(x)),
  7.                   x => f(x));
  8.       foreach (var e in q) a(e);
  9. </span>
Nessun trucco: semplicemente, si tratta di sostituire ogni chiamata alla classe System.Linq.Enumerable con una chiamata a System.Linq.ParallelEnumerable. In buona sostanza, chi ha sviluppato un'applicazione con LINQ, la renderà multiprocessore con un Trova e Sostituisci. I mesi di lavoro, lasciamoli pure agli sviluppatori non-.NET.

La seconda sintassi trae vantaggio dagli extensions methods di C# 3.0, e si applica in maniera naturale alla sintassi compatta delle query LINQ. La stessa query di cui sopra poteva essere espressa nella seguente forma:
  1. <span style="font-size:1.0em">
  2.       IEnumerable data = ...;
  3.       var q = data.Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
  4.       foreach (var e in q) a(e);
  5. </span>
alla quale corrisponde la naturale implementazione parallela:
  1. <span style="font-size:1.0em">
  2.       IEnumerable data = ...;
  3.       var q = data.AsParallel().Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
  4.       foreach (var e in q) a(e);
  5. </span>
Tutto quello che abbiamo fatto è stato invocare il metodo AsParallel sull'enumeration LINQ, nel suo overload più usato, quello senza parametri. Anche qui, trasformare la propria applicazione in una in grado di girare su sistemi multiprocessore diventa una semplice questione di Trova e Sostituisci. La bellezza del metodo AsParallel è che non si applica solo alle enumeration LINQ. Consideriamo la seguente funzione, che anagramma le lettere in un array di parole:
  1. <span style="font-size:1.0em">
  2.       Public Shared Function Scramble( _
  3.       ByVal words As String()) As String()
  4.           Return (From word In words _
  5.                   Select ScrambleWord(word)).ToArray()
  6.       End Function
  7. </span>
La sua versione parallela è semplicemente:
  1. <span style="font-size:1.0em">
  2.       Public Shared Function Scramble( _
  3.       ByVal words As String()) As String()
  4.           Return (From word In words.AsParallel() _
  5.                   Select ScrambleWord(word)).ToArray()
  6.       End Function
  7. </span>
Anche qui, una semplice questione di Trova e Sostituisci. Naturalmente, come ben sa chi si occupa di calcolo parallelo, a differenza della programmazione ordinaria non è garantito che l'ordine degli output di elaborazione sia lo stesso degli input. PLINQ ha però una semplice soluzione Trova e Sostituisci anche per questo problema, e consiste in un banale overload di AsParallel, che accetta un valore ParallelQueryOptions:
  1. <span style="font-size:1.0em">
  2.       Public Shared Function Scramble( _
  3.       ByVal words As String()) As String()
  4.           Return (From word In words.AsParallel( _
  5.                       <b>ParallelQueryOptions.PreserveOrdering</b>)_
  6.                   Select ScrambleWord(word)).ToArray()
  7.       End Function
  8. </span>
L'unico limite di PLINQ, del resto comune a tutte le librerie di calcolo parallelo, sta nelle query che per via di effetti collaterali non sono parallelizzabili, dettagliatamente descritte nella sezione "Parallelism Blockers" della documentazione.

A dispetto della sua incredibile potenza espressiva, esistono degli scenari, legati alla presenza di codice legacy che fa uso del parallelismo imperativo di dati e task e che è necessario migrare, in cui PLINQ è una soluzione efficace, ma non ottima; per tali casi (dai quali è esclusa ogni applicazione già sviluppata facendo uso di LINQ) le Parallel Extensions for .NET Framework espongono la Task Parallel Library ed i suoi due namespace, System.Threading e System.Threading.Tasks, contenuti anch'essi nell'assembly System.Threading.dll.

Nessun commento:

Posta un commento