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:
<span style="font-size:1.0em">
IEnumerable data = ...;
var q = Enumerable.Select(
Enumerable.OrderBy(
Enumerable.Where(data, x => p(x)),
x => k(x)),
x => f(x));
foreach (var e in q) a(e);
</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:
<span style="font-size:1.0em">
IEnumerable data = ...;
var q = ParallelEnumerable.Select(
ParallelEnumerable.OrderBy(
ParallelEnumerable.Where(data, x => p(x)),
x => k(x)),
x => f(x));
foreach (var e in q) a(e);
</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:
<span style="font-size:1.0em">
IEnumerable data = ...;
var q = data.Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
foreach (var e in q) a(e);
</span>
alla quale corrisponde la naturale implementazione parallela:
<span style="font-size:1.0em">
IEnumerable data = ...;
var q = data.AsParallel().Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
foreach (var e in q) a(e);
</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:
<span style="font-size:1.0em">
Public Shared Function Scramble( _
ByVal words As String()) As String()
Return (From word In words _
Select ScrambleWord(word)).ToArray()
End Function
</span>
La sua versione parallela è semplicemente:
<span style="font-size:1.0em">
Public Shared Function Scramble( _
ByVal words As String()) As String()
Return (From word In words.AsParallel() _
Select ScrambleWord(word)).ToArray()
End Function
</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:
<span style="font-size:1.0em">
Public Shared Function Scramble( _
ByVal words As String()) As String()
Return (From word In words.AsParallel( _
<b>ParallelQueryOptions.PreserveOrdering</b>)_
Select ScrambleWord(word)).ToArray()
End Function
</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.