MapReduce e TPL

Nel precedente post ho illustrato la "teoria" che si cela dietro all'algoritmo di "MapReduce". Ora veniamo al lato pratico della cosa.

Prima di tutto è necessario pensare a come implementare i personaggi che partecipano alla nostra scenetta: il nostro palazzo, il coordinatore, e gli assistenti. Il palazzo, ovviamente non può essere che un dato enumerabile; il coordinatore è la classe che implementa fisicamente l'algoritmo di MapReduce; e, per finire, i nostri assistenti saranno delle entità che possono lavorare contemporaneamente: dei banalissimi thread.

Il concetto di "thread" e "parallelismo" è molto comune nell'informatica. Negli ultimi anni, cioè da quando ci si è avvicinati troppo al limite fisico della velocità dei processori, si è cercato di migliorare la potenza dei sistemi andando nella direzione di aumentare il numero delle CPU, piuttosto che spingere ulteriormente sull'acceleratore del "clock".

Con la parellizzazione sono nate certo nuove possibilità, ma anche nuove difficoltà che comportano uno sviluppo del software più attento e rispettoso della "fisica" della macchina su cui il nostro progetto andrà a girare.

Per fortuna la versione 4.0 di .NET ci viene incontro con le cosidette TPL: Task Parallel Library. Non mi voglio dilungare nell'illustrazione del loro funzionamento perchè in rete potete trovare una miriade di articoli ben scritti sulle peculiarità di questo piccolo gioiello; vi basti sapere che semplificano di molto la gestione dei thread paralleli che, naturalmente, potevano essere manipolabili anche con metodi tradizionali, benchè con non poche difficoltà.

Bene. Ora che abbiamo presentato la nostra squadra, direi che possiamo iniziare cercando di fare subito le cose per bene. Non sono uno a cui piace riscrivere le cose mille volte prima di arrivare alla versione definitiva, quindi pensiamo subito ad una soluzione che sia sufficientemente generica e riutilizzabile.

Realizzeremo il nostro "Processore MapReduce" attraverso una classe .NET, che dovrà rispondere ai requisiti definiti dall'interfaccia:

L'interfaccia espone tre proprietà in sola lettura: "Input" rappresenta i dati da dare in pasto al processore, "NumberOfProcessors" il numero di assistenti che sarà utilizzato, e "PartialResults" i risultati parziali calcolati da ogni assistente.

Il metodo "Execute" non fa altro che mandare in esecuzione l'operazione di calcolo a fronte delle configurazioni impostate nella classe, emettendo in uscita la computazione finale.

E' evidente come le proprietà in sola lettura non possono che essere valorizzate nel costruttore della nostra classe di MapReduce; quindi, prima di procedere all'implementazione, vediamo come sarà il risultato finale e l'utilizzo della classe che andremo a creare.
Evitiamo di rendere le cose difficili, quindi predisponiamo uno scenario banalissimo. Creaiamo dei dati di esempio utilizzando un ipotetico oggetto "Floor" che rappresenta un singolo piano del nostro palazzo; tale "Floor" avrà un'unica proprietà che contiene il numero di persone che lavorano sul piano stesso.

Il nostro processore, chiamato per l'occasione "MapReducePeopleCounter", sarà istanziato tramite una classica "new", e gli saranno iniettati i dati da processare (la lista dei piani che compongono il palazzo), e il numero di assistenti che svolgeranno il lavoro di conteggio in nostra vece.

Come detto, il metodo elaborazione "Execute" esegue l'algoritmo definito nella sua implementazione, quindi emetterà il risultato voluto sulla console.
Sembra tutto facile; e deve esserlo se vogliamo che il nostro algoritmo ci possa aiutare nei momenti di necessità; sopratutto il suo utilizzo deve essere banale anche per un'altra persona che non deve necessariamente comprendere tutti i "risvolti" della tecnica di Map/Reduce per poterla utilizzare.

Ma vediamo l'implementazione diretta della nostra classe "MapReducePeopleCounter".

Subito possiamo notare che l'implementazione fornita è una superclasse della classe base "MapReduceProcessor", che rappresenta l'astrazione dell'algoritmo stesso; tale classe base richiede che siano specificati i tipi di dati in input e in output.

Come menzionato in precedenza il dato di input deve essere un enumerabile; in questo caso, visto che il nostro dominio dati sono i piani ("Floor") del palazzo, la tipologia unitaria del dato di input è a classe "Floor" stessa.

Di contro, il valore di output è un numero; un interno che rappresenta il numero di persone totali contenute nel palazzo.

Il costruttore della classe "MapReducePeopleCounter" utilizza il "base" della classe da cui eredita, dovendo accettare in ingresso la lista dei piani e il numero di processori (assistenti) di cui ci dovremo servire.

Infine, l'implementazione è caratterizzata dai due metodi che definiscono l'argoritmo di conteggio di ogni singolo assistente (stiamo parlando di "Process") e quello di aggregazione dei risultati parziali ("Reduce").

Spediamo due parole su "Process". Il metodo riceve in ingresso due parametri: "partialInput" è un parziale dei dati iniettati nel costruttore della classe, e "processorId" rappresenta l'identificativo dell'assistente che sta eseguendo l'algoritmo stesso. Immagino che a questo punto sarete confusi (io lo sono ;) ): "Ma non avevamo detto che il numero di assistenti utilizzato doveva essere pari al numero di piani da contare?". Certamente si. Ma vogliamo essere flessibili al massimo, quindi abbiamo previsto la possibilità che un palazzo di 100 piani possa anche essere elaborato da solo 10 assistenti: 10 piani ciascuno e il gioco è fatto!

"Reduce" (si...quello di "Map/REDUCE") è un metodo che esegue l'aggregazione delle informazioni calcolate dagli assistenti, e aggrega il tutto in un conteggio totale che sarà l'output dell'intera elaborazione. Chiaramente il coordinatore non può stare con le mani in mano...quindi sarà sua responsabilità prendere i valori parziali, e sommarli per ottenere il conto totale delle persone nel palazzo.

Non vorrei farlo...ma dobbiamo chiudere qui per questa volta. Nel prossimo post cercherò di illustrare come utilizzare le fantastiche funzioni di Task Parallel Library per realizzare la nostra classe astratta "MapReduceProcessor" e finalmente vedere la luce in fondo al tunnel.
See you soon...

Commenti

Post popolari in questo blog

Restore di un database SQL Server in un container Docker

Cancellazione fisica vs cancellazione logica dei dati

WCF RESTful service: esposizione del servizio