Programmazione Parallela

Programmazione Parallela

Introduzione
Nel corso degli anni di evoluzione dei computer, si è assistito a una progressione dei sistemi da processori single core a multi-core, consentendo il concetto di elaborazione parallela. L’elaborazione parallela consente ai sistemi informatici di eseguire realmente più attività contemporaneamente grazie alla presenza di più processori/core.

Con questa capacità nasce il concetto di programmazione parallela, in cui gli sviluppatori scrivono programmi che scompongono problemi più grandi in parti più piccole, indipendenti e spesso simili, che possono essere eseguite simultaneamente da più processori che comunicano tramite memoria condivisa; i risultati vengono combinati al termine come parte di un algoritmo complessivo.

I supercomputer e i cluster di calcolo ad alte prestazioni non sono gli unici dispositivi che possono usare la programmazione parallela. Viene spesso usata anche nelle tecnologie più standard.

Ad esempio, i browser web moderni utilizzano l’elaborazione parallela per gestire le richieste di rete, eseguire HTML/CSS ed eseguire JavaScript contemporaneamente su più core della CPU, con conseguente rendering più rapido delle pagine web.

I motori dei videogiochi sfruttano il parallelismo per imitare le interazioni dell’intelligenza artificiale in tempo reale, la fisica e i paesaggi complessi.

Parallelismo vs Concorrenza
Parallelismo e concorrenza sono spesso confusi, in quanto apparentemente sembrano avere lo stesso significato, ma nel mondo informatico i due termini hanno significati leggermente diversi.

La concorrenza trasmette l’illusione di parallelismo durante l’elaborazione, nel senso che un core è in azione ma consente la sovrapposizione e l’interlacciamento di più attività/thread in un lasso di tempo impercettibilmente breve, come se venissero eseguite nello stesso istante, mentre il parallelismo comporta che più processori eseguano attività diverse contemporaneamente.

Possiamo immaginare la concorrenza e il parallelismo come una sessione di cucina. Diciamo che vuoi preparare un buffet per un gruppo di persone, quindi assumi uno chef (che rappresenta un processore) per preparare tutti i pasti.

Idealmente, il nostro chef può fare solo una cosa alla volta: in un momento taglia delle verdure, in un altro fa bollire qualcosa e in un altro ancora frigge qualcosa (che rappresentano i compiti).

Per magia, lo chef sta svolgendo tutte e tre le attività contemporaneamente, ma in realtà può gestire solo una cosa alla volta. Contrariamente a un solo chef che prepara tutti i pasti, potresti assumere più chef per la preparazione e far svolgere azioni diverse da chef diversi contemporaneamente (vero parallelismo), il che produrrebbe un output più rapido in condizioni ideali.

Ecco come funzionano i processori. In verità, un processore può eseguire solo un’attività alla volta, ma il tempo di elaborazione è così rapido che quando si impiega la concorrenza, sembra che vengano eseguite più attività in quell’istante.

Possiamo ottenere il parallelismo con più processori, dove un’attività viene suddivisa in più thread ed eseguita su processori diversi e il risultato viene presentato come un unico risultato dopo l’esecuzione, per ridurre ulteriormente i tempi di elaborazione e aumentare l’efficienza.

Programmazione in parallelo
Java fornisce vari meccanismi per consentire agli sviluppatori di scrivere programmi paralleli in modo efficiente.

  1. Framework Fork/Join: il framework Fork/Join di Java, introdotto in Java 7, migliora le prestazioni del programma utilizzando tutti i core di elaborazione disponibili nel sistema e segue un approccio di tipo “dividi et impera” per elaborare i programmi in parallelo.
  2. Pool di thread personalizzati: con l’interfaccia “ExecutorService”, gli sviluppatori possono creare pool di thread personalizzati e specificare il livello di parallelismo desiderato in base ai core della CPU disponibili
  3. API Parallel Streams: Java consente agli sviluppatori di parallelizzare le operazioni sulle raccolte sfruttando le CPU multicore tramite la suddivisione del lavoro tra i thread. La parallelizzazione viene gestita automaticamente, ma l’ordine dei risultati delle operazioni non è garantito.

Programma Java che utilizza flussi paralleli:

Programmazione Parallela 2

Nel codice sopra, stiamo generando un array di interi casuali e trovando il valore massimo usando la stream API di Java. L’implementazione parallela è gestita per noi e vediamo che vengono prodotti gli stessi risultati per entrambi i tipi di stream.

Si nota che il tempo impiegato per completare ogni attività cambia con la stessa frequenza con cui si esegue il codice, a volte con il flusso sequenziale che impiega meno tempo. Si nota una differenza di tempo significativa tra flussi paralleli e sequenziali in applicazioni che utilizzano un ampio set di dati complesso ed eseguono operazioni più complesse.

Vantaggi della programmazione parallela

  1. Maggiore velocità ed efficienza: i calcoli complessi e le procedure ad alta intensità di dati vengono eseguiti più rapidamente quando le attività sono suddivise su più unità di elaborazione rispetto a quando sono programmate in sequenza, il che riduce i tempi di elaborazione.
  2. Utilizzo delle risorse: con più unità di elaborazione, si verifica un elevato utilizzo delle risorse nei sistemi multiprocessore, garantendo che nessuna potenza di elaborazione resti inutilizzata e che venga sfruttato il pieno potenziale delle risorse hardware.
  3. Scalabilità: i programmi paralleli possono scalare in modo efficiente per gestire set di dati più grandi o calcoli più complessi. Man mano che aumenta il numero di unità di elaborazione, i programmi paralleli possono distribuire le attività tra queste unità, utilizzando efficacemente le risorse disponibili e mantenendo le prestazioni man mano che le dimensioni del carico di lavoro aumentano. Questa scalabilità è essenziale per le applicazioni che gestiscono big data o elaborazione ad alte prestazioni.

Svantaggi della programmazione parallela

  1. Maggiori requisiti di risorse: contrariamente alla programmazione sequenziale che richiede solo un’unità di elaborazione per eseguire completamente un’attività, la programmazione parallela comporta l’utilizzo di almeno due unità di elaborazione per ottenere il parallelismo che può essere necessario o meno a seconda del caso d’uso. I programmi paralleli spesso richiedono più memoria per memorizzare risultati intermedi, buffer di comunicazione e primitive di sincronizzazione. Ogni attività parallela può aver bisogno delle proprie strutture dati e spazio di memoria, portando a un maggiore consumo di memoria rispetto ai programmi sequenziali.
  2. Overhead di comunicazione: nella programmazione parallela, le attività spesso devono comunicare tra loro per condividere dati o sincronizzare la loro esecuzione. Questo overhead di comunicazione può essere significativo, specialmente in ambienti di elaborazione parallela distribuita in cui le attività possono essere eseguite su macchine separate connesse tramite una rete. La comunicazione implica il trasferimento dei dati, la serializzazione, la deserializzazione e i meccanismi di sincronizzazione, tutti i quali richiedono risorse di elaborazione aggiuntive.
  3. Complessità di progettazione, codifica e debug: richiede una grande comprensione per scrivere in modo efficiente programmi che verranno eseguiti in parallelo rispetto ai programmi sequenziali. Con il comportamento non deterministico della programmazione parallela dovuto all’esecuzione intervallata di attività parallele, è difficile riprodurre e diagnosticare i bug.

Considerazioni chiave
Se desideriamo utilizzare regolarmente la programmazione parallela, è importante valutare se il parallelismo è necessario per la nostra sfida e se i possibili vantaggi superano la maggiore complessità.

Ecco alcune cose fondamentali da ricordare:

  1. Determiniamo quali task possono essere parallelizzati: non tutti i task sono meglio eseguiti in parallelo. Decidiamo quali parti del tuo programma possono essere parallelizzate con successo, quindi concentrati sul rendere quelle aree il più efficienti possibile. Cerchiamo task con un’elevata richiesta computazionale e poche risorse dipendenti.
  2. Misuriamo e benchmark: valutiamo le prestazioni della versione sequenziale prima di parallelizzare il codice. Quando si distribuiscono soluzioni parallele, il benchmarking fornisce una base di riferimento per il confronto e aiuta a identificare i colli di bottiglia.
  3. Iniziamo dalle basi: iniziamo con un parallelismo su piccola scala e aggiungi più complessità man mano che procedi. Per sentirci a nostro agio con i costrutti paralleli ed evitare una complessità schiacciante, sperimentiamo in un ambiente controllato con le librerie.
  4. Scalabilità della mente: prendiamo subito in considerazione la scalabilità. Assicuriamoci che le dimensioni del problema e le risorse hardware disponibili possano essere gestite in modo efficiente dalla tua soluzione parallela. Cerchiamo soluzioni che si adattino bene a vari numeri di processori e livelli di carico di lavoro.

(fonte)

Innovaformazione, scuola informatica specialistica promuove la formazione per sviluppatori rivolgendosi in primis alle aziende. Trovate altri articoli di settore navigando sul nostro blog, QUI. L’elenco corsi è presente sul nostro sito QUI.

INFO: info@innovaformazione.net – Tel. 3471012275 (Dario Carrassi)

Ti potrebbe interessare

Articoli correlati