Upload
others
View
0
Download
0
Embed Size (px)
Citation preview
Modelli
Ciò che ci importa nei sistemi distribuiti e tutto ciò che ha impatto durante l'esecuzione e che
rimane significativo e vitale durante questa favorendo ed abilitando la distribuzione.
9
Nel distribuito si privilegia sempre l'uso di modelli dinamici, ma ciò rende molto difficile la
gestione.
Esempio:
Supponiamo un sistema composto da molti servizi, come fanno i clienti a conoscere i servitori?
In questo contesto avremmo bisogno di una serie di gestori (ad esempio gestori di nomi) in grado di
dirci chi sono e dove trovarli.
Ragionando quindi in maniera ingegneristica, quando si aggiunge una certa caratteristica, quello di
cui bisogna tener conto è quanto costa la caratteristica stessa.
10
Monoutente: le decisioni dell'unico utente presente si concentrano su un unica risorsa.
Multiutente: le decisioni di più utenti di distribuiscono in maniera bilanciata utilizzando tutte
le risorse in modo corretto. Questo modo di lavorare è particolarmente adatto per i sistemi di
gestione in cui bisogna tener sotto controllo più risorse, introducendo problemi di sicurezza.
Quando si parla di come si usano i processori, si può lavorare in due modi:
modello workstation: ossia quando si lavora alla propria macchina personale. In generale,
quando si lavora solo su risorse di una particolare macchina, il che non evita di poter spostarsi su
altre macchine continuando quindi a lavorare unicamente su risorse allocate su tale macchina.
Processor pool: è un sistema composto da una serie di processori i quali possono essere
usati da chiunque, quindi possono essere acceduti da qualsiasi postazione e macchina. Ciò che
succede in tale modello è che le risorse, alle quali come già detto possono accedere chiunque da
postazioni differenti, sono messe a disposizione da utenti diverse situati su macchine diverse.
Per tale motivo tale modello è decisamente più idoneo per il distribuito.
Per ciò, la gestione di un modello a processor pool è più complicata e difficoltosa.
11
I processi, accedono e rendono disponibile l'accesso, a delle risorse che possono essere:
private: del processo stesso, quindi trattate solo da quest'ultimo con inevitabile assenza di
colli di bottiglia nel loro utilizzo (ad esempio sue variabili) perché si sta facendo riferimento ad
azioni per l'appunto locali.
Condivise: con le quali bisogna fare attenzione per la possibile presenza di interferenze
dovute al loro utilizzo da parte di altri utenti. In questo caso avvengono azioni di comunicazione con
altri processi attraverso memoria condivisa e scambipiè di pagina pari dispari diverso di messaggi;
si usano dati esterni ai processi stessi.
Reti di Calcolatori LM 1
Non si può parlare di processi senza parlare di oggetti: contenitori di informazioni.
Un OGGETTO è qualche cosa, tipicamente, che ha il suo contenuto non accessibile a tutti perché
presenta vincoli d'accesso. Tale contenuto quindi non è accessibile direttamente dall'esterno
(principio di astrazione) se non con metodi che permettono di accedere alle risorse dell'oggetto
stesso.
Gli oggetti si distinguono in oggetti che eseguono (oggetti attivi) ed oggetti invece, che hanno solo
del contenuto e che aspettano di essere acceduti (oggetti passivi); per tale motivo, potrebbero
avvenire problemi di interferenza di processi che accedono agli oggetti. Pertanto un oggetto deve
avere conoscenza di come viene acceduto!
<<Come può un oggetto sapere chi e come questo chi l'ha acceduto?>>
A tal proposito, vi sono modelli in cui un oggetto permette di essere acceduto se e solo se é lui
stesso a “darne il permesso”.
12
Il modello ad oggetti passivo è di Java.
É un modello in cui gli oggetti, contenitori d' informazioni, aspettano di essere acceduti, il che può
portare ad una situazione in cui più oggetti esterni accedano ad un unico oggetto cambiandone lo
stato (nella slide le frecce rappresentano per l'appunto l'accesso all'oggetto).
Tale modello non è quindi un modello molto astratto dal punto di vista dell'esecuzione proprio
perché quando ne scrivo il codice, devo sapere se tale oggetto può essere acceduto da uno o più
oggetti ed in tal caso se lo possono fare contemporaneamente: modello a scarso confinamento.
Nel modello ad oggetti attivi , in linea di principio, è l'oggetto stesso che decide chi far entrare e
quando farlo entrare.
13 - 14
In generale, un oggetto è composto da metodi e dallo stato e per stato si intendono le variabili
dell'oggetto, ossia il contenuto, al quale ci si può accedere invocando un metodo dell'oggetto.
Se l'oggetto è
Passivo:
un processo arriva, ne richiede il metodo e modifica le variabili. Il tutto avviene in maniera
incontrollata.
Attivo:
tutta la parte di visibilità ed esecuzione del metodo, non è più visibile all'esterno.
In quest'ultimo caso ci sarà quindi tutta una parte di supporto per la gestione dell'accesso all'oggetto
e delle richieste degli oggetti che arrivano dall'esterno. Tale supporto funge da filtro e decide chi far
passare e chi no: avremo quindi una coda delle richieste (politica FIFO: dipenderà dal contenuto, ad
esempio se qualità differenziata o no...). Così facendo la visibilità dall'esterno viene spezzata, e la
politica di scheduling viene decisa all'interno dell'oggetto che viene quindi conseguentemente
protetto da interferenze esterne.
Nel distribuito avere degli oggetti ATTIVI è particolarmente ottimo!
15
Ragionando ora sui dati, questi si distinguono in:
2 Stefano Di Monte
• primitivi: insieme di valori
• riferimenti ad altri oggetti: che a loro volta possono essere:
� per riferimento: non sono dei valori ma puntatori ad altri oggetti che sono quindi riferiti
da questo.
� Per contenimento: ossia che un oggetto è realmente dentro un altro oggetto (legame
molto forte). (I primi middleware lavoravano in questo modo.)
17
Supponiamo di avere delle istanze con caratteristiche comuni e che quindi hanno lo stesso
comportamento, quest possono conseguentemente esser messe a fattor comune: vi è quindi una
classe, un contenitore descrittivo, che dice quali sono i valori delle variabile delle istanze che a loro
volta rappresentano i metodi i quali possono essere invocati su ciascuno di questi valori dalle
singole istanze.
CLASSE= contenitore descrittivo di una serie di istanze.
<<Vi sono classi a run-time?>> Si! (ad esempio in Java sono caricate dinamicamente, quindi su
bisogno.)
<<Dove?>> La JVM tende a caricare le classi nell' heap.
<<Quando?>> Quando un'istanza di quella classe dev'essere caricata (esigenza vera). Di
conseguenza, per creare l'oggetto O1 della classe C1, dovremmo prima caricare la classe C1.
Quando poi vengono scaricate, ossia quando non vi è più necessità (o anche no!), viene liberata
memoria che sarà usata quindi, per caricare altre classi.
18
Come per molti altri linguaggi ad oggetti, si prevede un'ereditarietà:
data una serie di istanze, i valori di queste istanze sicuramente sono specificate nella classe C, ma
potrebbero essere specificate anche in una super classe SC: si forma così una catena con una classe
e possibilmente molte (ereditarietà multipla) super classi. (NOTA: in Java vi sono solo catene ad
ereditarietà semplice ossia con una sola super classe).
L'ereditarietà multipla comporta certe problematiche (come ad esempio la necessità di specificare il
percorso esatto dell'albero di ereditarietà da dover percorrere per accedere alle istanze di una certa
classe), problematiche che hanno un certo impatto sull'esecuzione, sicuramente in maniera negativa.
Difatti, il problema di quale percorso percorrere su di un certo albero di ereditarietà, nel distribuito
non è un problema da poco perché le classi potrebbero (e sicuramente lo sono) essere sparate su
nodi diversi, comportando la presenza di tempi molto forti di ritardo.
Per tale motivo le classi vengono tenute, laddove è possibile, tutte insieme, compattate (assieme alle
super classi) in un unico contenitore che sarà quindi la descrizione di tutte le sue caratteristiche.
20 - 21 - 22
Pertanto l'ereditarietà introduce problemi di gestione!
<<Concludendo, quale delle due ereditarietà, tra quella semplice e quella multipla, è migliore?>>
L'ereditarietà è un ottima scelta dal punto di vista della modellazione. Se volessimo, ad esempio,
data una classe ed una sua super classe, avere un ulteriore comportamento (del sistema), in
ereditarietà semplice saremmo costretti a riscrivere il tutto mente il ereditarietà multipla no.
Reti di Calcolatori LM 3
D'altro canto, l' overhead di gestione introdotto dall'ereditarietà multipla ha favorito, da questo punto
di vista, lo sviluppo dell'ereditarietà semplice.
Se ci si basa sul grado di accettazione dell'utente finale (che è ciò che conta in termini di distanza
temporale), gli strumenti semplici sono (quasi) sempre i migliori!
23
Quando lavoriamo nel distribuito, lavoriamo sempre con dei componenti: classi, oggetti ed
interfacce. Quest'ultima è la specifica di un contratto, non potrebbe avere un'implementazione a
differenza delle classi che invece rappresentano qualcosa di più concreto.
L'interfaccia quindi, rappresenta solo un contratto, la descrizione/firma dei metodi con i quali si può
accedere all'interno del contenitore/oggetto.
In generale, nel distribuito, quando si parla di un componente (ad esempio della classe C) e
quest'ultima ha 4 super classi in ereditarietà (possibilmente anche in catena) molto spesso i metodi
(organizzati tipicamente su una altro nodo) vengono raggruppati tutti insieme (come se avessi un
unico contenitore con i metodi della classe e delle super classi) per facilitarne la navigazione. Tale
contenitore potrebbe essere condiviso poi da tutte le istanze.
Ciò facilita un possibile spostamento (!!) del contenitore stesso, essendo quest'ultimo molto
compattato.
C'è però un problema:
Nell'invocare un qualche metodo di un certo componente, devo sapere come riferirlo e, non
potendoci essere una gestione semplice, le cose si complicano leggermente.
Ad esempio, potrebbe succedere che in due contenitori diversi ci siano gli stessi metodi (ad esempio
della classe desc1) ma in posizioni diverse: spiazzamento dipendente dal linguaggio,
dall'ereditarietà utilizzata....
<<Come posso, quindi trovare i metodi che m'interessano?>>
24
In generale, quando si compatta tutto in un contenitore (per i sistemi che lo fanno), si decide che
l'accesso all'oggetto avvenga attraverso una tabella unica in cui vi sono i puntatori ai metodi
necessari!
Ogni singolo componente ha quindi, dei puntatori ai suoi metodi necessari!
Così facendo, si risolve il problema della posizione, in quanto, se volessi richiedere un metodo della
classe 1 andrei ad uno spiazzamento di +1 rispetto alla posizione 0 in cui ci sono i metodi della
classe corrente.
Attenzione:
“Spiazzo” che però potrebbe essere DIFFERENTE per componenti diversi!
25
Tutti i sistemi che lavorano in dinamico, per poter ritrovare le cose di interesse, mettono a
disposizione delle API (in questa slide chiamata in modo astratto) che consentono, per l'appunto, di
poter ritrovare ciò che si vuole. Tali funzioni vengono invocate dal supporto per ottenere la
focalizzazione e la posizione dei metodi di interesse.
26
Questa funzione è nascosta dal supporto, non visibile dall'utilizzatore finale, anche se in alcuni casi
bisogna che ci sporchiamo le mani, bisogna quindi conoscerla, come succede in DCOM.
4 Stefano Di Monte
Nell'ambito di DCOM vi è una funzionalità, la QueryInterface, che è parte dell'interfaccia di base di
ogni componente.
27
semantica per riferimento: gli oggetti non contengono altri oggetti, bensì riferimenti ad altri oggetti;
si creano quindi delle catene tra gli oggetti stessi
28-->32
Ci potrebbero quindi essere riferimenti reciproci, tra gli oggetti; più precisamente, ciò che si intende
dire è che un oggetto, sicuramente ha sue variabili (ad esempio primitive), ma è possibile che ne
veda degli altri perché ha un riferimento affinché gli altri lo cerchino.
Supponendo di voler fare una macchina virtuale, quello che si potrebbe pensare è di voler far sì che
tale riferimento sia locale (abbreviato ad esempio in un indirizzo di memoria), ovviamente in questo
scenario c'è un problema quando si porta tutto nel distribuito perché la semantica è locale e non la
posso usare intra-nodo. C'è una sostanziale disomogeneità tra quello che vedo localmente e quello
che vedo non localmente.
<<In java i riferimenti locali sono statici?>>
In generale non sono risolti staticamente bensì dinamicamente.
Come prima soluzione è stato pensato di inserire una specie di via di fuga, ossia di comunicare con
gli strati di comunicazione, attraverso ad esempio delle socket, portando alla creazione di oggetti
ottenuti come risultato di una ricezione , aumentando il dominio della visibilità locale (perché creo
delle cose nuove). Tale modo di lavorare è significativo perché nel mio dominio aggiungo delle cose
nuove ma d'altronde si complica la gestione del tutto perché, naturalmente, bisogna sapere con chi
stiamo comunicando, di conseguenza c'è bisogno di un sistema di nomi.
Come si nota, già a questo livello abbiamo bisogno dell'aiuto di un qualche supporto.
Questo primo modo di lavorare è però insufficiente per ciò che vorremmo noi avere, ossia riuscire,
in modo più trasparente all'utente, a vedere delle entità locali e (circa allo stesso modo) remote:
vogliamo avere dei riferimenti remoti (fondamentali nel distribuito).
Un riferimento remoto lo si implementa, ad esempio, attraverso il principio di delega: l'utente crede
che stia riferendo direttamente un' entità remota, ma in realtà vi è un intermediario locale (proxy)
che si preoccupa di passare le cose dall'altra parte; non si parla di trasporto di qualcosa dall'altra
parte bensì di capacità di portare delle cose dall'altra parte.
Normalmente questo schema prevede due proxy, uno lato cliente ed uno lato servitore, che
coordinano il lavoro e permettono il riferimento remoto.
Quindi ci dev'essere qualcuno che si preoccupa di creare i proxy i quali lavorano durante
l'esecuzione.
Così facendo abbiamo bisogno di fare delle previsioni molto precise di con chi si comunicherà in
remoto!
Ragionando sui tempi di vita, in generale lavorando in locale, quando ho un riferimento per un'altra
entità, tipicamente questa entità deve esistere, e se per caso qualcuno la distrugge sappiamo con
certezza che a livello locale tipicamente questo non succede: se per assurdo succedesse, non avrei il
riferimento giacché passerebbe il garbage collector e lo eliminerebbe; in remoto, invece, il
riferimento remoto è più lasco di quello locale, ad esempio potrei aver preparato la struttura di
supporto ma potrei non avere il servitore dall'altra parte magari per mancanza di coordinamento tra i
due nodi.
Reti di Calcolatori LM 5
Vi sono comunque, altri modelli da prendere in considerazione, ad esempio:
<<il proxy C1 per un istanza C1, potrebbe essere usato per un altra istanza dello stesso tipo C2 sullo
stesso nodo N1? In altre parole, un proxy potrebbe servire più richieste di quel nodo verso un altro
nodo?>>
Certo!
Però potrebbe anche essere che sia necessario avere un proxy per ogni possibile riferimento, con
possibile conseguente esplosione del numero dei proxy...aumentando la difficoltà di gestione del
tutto. Tutto dipende quindi dal contesto.
<<Cosa succede in RMI se qualcuno ha spento il servitore?>>
Abbiamo un eccezione!
Ciò non ci piace, di conseguenza ciò che vogliamo è che se facciamo una richiesta ad un server
remoto, questo debba essere acceso e funzionante!
Sicuramente quando lavoriamo con riferimenti remoti abbiamo bisogno di un sistema di nomi, i
quali possono essere molto vari ma che comunque svolgono un ruolo essenziale.
Supponendo di aver fatto un riferimento remoto in RMI e per tale riferimento supponiamo di aver
fatto un metodo con il quale ovviamente passiamo delle informazioni all'altro, ad esempio dati
primitivi o delle istanze di certe classi (contenuto un po' più organizzato), potrebbe essere che
dall'altra parte tale classe si conosca, che sia quindi la stessa uguale classe, ma non c'è alcuna
garanzia che sia perfettamente uguale (ci potrebbe essere un metodo in più o in meno o delle
variabili che da una parte ci sono e dall'altra parte no!), al fine di sapere se le classi sono identiche
una prima soluzione che viene in mente è fare del matching tra le classi (fare un hash della classe
prima di passarla e poi controllare l'hash a passaggio avvenuto dall'altra parte). Ma questa non è
l'unica soluzione. Vi sono soluzioni più complicate che rendono la durata dell'applicazione
superiore.
Parlando di modelli, di middleware, bisogna sempre pensare a cosa c'è sotto, ossia che i riferimenti
remoti richiedono un supporto al di sotto, senza il quale non possiamo lavorare, un supporto che
prenda delle decisioni, che preveda dei costi (più o meno alti) ma che permetta il buon
funzionamento del tutto, garantendo la mancanza di errori.
33
Supponendo, ora, di avere un applicazione, magari scritta in un unico linguaggio (ciò è molto
limitativo: difatti quasi mai realmente vero).
Ciò che ci aspettiamo è che, dopo il deployment effettuato perché abbiam necessità e volontà di
eseguire, il programma sviluppato possa essere adatto per diversi deployment e non per uno solo,
questo perché l'intera applicazione, spesso e volentieri, non ci sta su un solo nodo ma è divisa su
nodi diversi (con deployment diversi). Ciò che vogliamo, in sintesi, è poter cambiare la
configurazione dell'applicazione senza cambiare le singole sottoparti.
Supponendo ora di avere C1 e C2 su di uno stesso nodo e con riferimenti reciproci, è ovvio che
questi possono essere intesi come riferimenti locali, cosa che non sarebbe vera se fossero su nodi
diversi (riferimenti remoti in questo caso), devo quindi sapere come sono distribuiti i componenti
affinché possa aver preparato i relativi proxy per la comunicazione comportando un relativo costo
aggiuntivo.
Dover mettere dei componenti che interagiscono molto su uno stesso nodo, potrebbe essere una
decisione sbagliata perché, così facendo, i tempi di esecuzione dei due processi sarebbero in mutua
6 Stefano Di Monte
esclusione e non sovrapposti come se fossero su due nodi diversi (impatto sui tempi di esecuzione
finali), d'altro canto, dato che la comunicazione tra i componenti si svolge attraverso una banda, ed
essendo questa infinita se i due processi sono su di uno stesso nodo potrebbe essere un ottima
soluzione, ad esempio, per due processi che comunicano poco ma spesso (?!) (su due nodi diversi la
banda è limitata).
34
Quando progettiamo un'applicazione, di solito questa è composta da entità statiche (create all'inizio
e che durano durante tutta l'applicazione -ad esempio i demoni, le variabili...-) e da risorse
dinamiche, attivate quindi su bisogno, e la cui creazione non è semplice da prevedere, e ciò aumenta
di conseguenza la complessità dell'applicazione stessa.
35
Quindi sulle risorse statiche, possiamo prendere delle decisioni anche costose perché avvengono
prima dell'esecuzione e soprattutto, non hanno impatto sull'esecuzione stessa, tuttavia ci sono delle
situazione in cui l'allocazione non può essere decisa in modo statico, perché, come abbiam detto,
tali situazioni si manifestano durante l'esecuzione! In realtà ciò non è sempre vero, in quanto posso
prendere delle decisioni prima dell'esecuzione, quindi in modo statico, circa risorse che saranno poi
create dinamicamente durante l'esecuzione. (Ad esempio nella JVM molto spesso le decisioni
vengono prese prima dell'esecuzione). Ciò potrebbe comunque essere una politica sbagliata nel caso
in cui ad esempio, C1 di N1 crei sempre su N2 ingolfandolo conseguentemente. La soluzione quindi
sarebbe quella di adottare politiche dinamiche con conseguente aumento dei costi. Esistono altresì
politiche ibride.
Ne deriva che, la politica statica è molto semplice ma anche molto rigida, al contrario di quella
dinamica che invece è molto più flessibile.
36
Come facciamo in soldoni a fare le cose?
Nella maggior parte dei sistemi, a mano, quindi staticamente, si decide come e dove dev'essere
messo un certo componente e ciò per tutti i componenti; tale modo di lavorare è molto pesante
perché per ogni singola risorsa bisogna decidere dove dev'essere messa.
La prima cosa che viene in mente, quindi, è quella di utilizzare linguaggi di script per eseguire il
deployment.
Vi son altresì approcci basati su modelli dell' applicazioni, attraverso i quali specificare i vincoli
necessari fra le diverse parti dell'applicazione stessa, tutto ciò avviene attraverso degli strumenti di
supporto.
37-38
Modelli di allocazione:
Approccio esplicito= comandato dall'utente che specifica il mappaggio dell'applicazione. Approccio
granulare ma che richiede un costo molto elevato dal punto di vista dell'utente stesso.
Approccio implicito= approccio automatico, l'utente non si occupa del deployment perché vi è
qualcun' altro che lo fa al posto suo. Quindi tale approccio prevede che ogni singola entità creata in
un programma trovi, grazie ad un supporto, la propria allocazione. Tale approccio dev'essere quindi
valido sia per le cose che succedono durante l'esecuzione sia per quelle cose che succedono prima
dell'esecuzione.
Reti di Calcolatori LM 7
Tali approcci sono agli estremi, di mezzo c'è l'approccio misto= alcune delle risorse statiche e/o
dinamiche vengono guidate dall'utente, tutto il resto viene gestito in modo implicito. Ciò semplifica
la vita all'utente. Tale sistema, in generale è il migliore proprio perché vi è un supporto al di sotto ,
che per la maggior parte delle cose (come ad esempio il bilanciamento delle risorse) fa delle
valutazioni e poi vi sono delle indicazioni dell'utente tenute in conto per migliorare le prestazioni
39
Il deployment è quindi un problema non semplice da affrontare, ma che incide molto sulle
prestazioni del sistema.
Ad esempio, per due componenti che debbano comunicare e che siano su di uno stesso nodo (locali)
possiamo quindi condividere la memoria ed anche dei file, delle socket, insomma tutta la parte del
sistema operativo. Tutto ciò non si può fare se siamo in remoto.
Pertanto, in locale non utilizzerò di sicuro RMI, come in remoto non utilizzerò sicuramente dei
riferimenti locali!
40-41
Il modello cliente/servitore è il modello più usato, nasconde tutta una serie di dettagli
implementativi che sono curati dal supporto.
Il modello c/s è un modello a forte accoppiamento, in quanto prevede che il cliente conosca il
servitore; inoltre prevede, di default, una comunicazione sincrona (che c'è il risultato) bloccante (che
c'è attesa per l'arrivo del risultato).
Una volta scelto il modello da utilizzare, succede sempre che vengono eseguite delle modifiche,
questo perché non sempre il modello scelto riesce ad incarnare tutte le situazioni possibili che
vengono in mente.
Alcuni modifiche sono:
Modello pull
Come è ovvio, è possibile che un cliente possa prendere l'iniziativa, pertanto in questo caso il cliente
stesso è tenuto a richiedere il servizio ad un servitore, quindi recuperare la risposta, se prevista.
Modello push
parte della responsabilità della risposta potrebbe essere fornita dal provider del servizio; è un
modello in cui il cliente non aspetta la risposta (dopo aver fatto richiesta è libero di fare ciò che
vuole), perché è compito del servitore di fornire la risposta che poi verrà recuperata localmente dal
cliente. Scambio ASINCRONO d'informazione. Da notare che il servitore può essere sincrono
bloccante o anch'esso asincrono.
Modello A delega
Potrebbe essere che il cliente non voglia aspettare il risultato, quindi, complicando un po' il tutto,
l'interazione prevede una terza parte, ossia qualcuno che possa aspettare il risultato, per l'appunto il
delegato. Il cliente, a questo punto recupererà il risultato da quest'ultimo, e potrebbe farlo anche
continuamente.
Modello a notifica
Stesso principio della delega, ma in questo caso il delegato con un messaggio di notifica, avvisa il
cliente dell'arrivo della risposta.
8 Stefano Di Monte
Il cliente è disaccoppiato in maniera forte dal risultato che verrà gestito dal proxy sottostante.
42
Lavorando sincrono (c'è il risultato) non-bloccante (non ci blocchiamo nell'attesa del risultato) è
possibile lavorare, come si dice, per delega, differenziando il caso in cui si lavora con il:
poll-object
il mittente chiede un operazione al servitore indicandogli un oggetto (il poll-object) destinato a
contenere il risultato. E' compito di tale oggetto di contenere il risultato destinato al cliente
originario; ci sarà quindi un qualche meccanismo (modello di notifica) che consente al cliente
originario di andare a prendere il risultato. Si dice che sitam lavorando poll per il recupero del
risultato.
Abbiamo quindi una interazione tra il poll-object e il cliente originario necessaria, senza la quale
non sarebbe possibile far arrivare il risultato al cliente stesso.
Call-back-object
L' oggetto destinatario del risultato è molto indipendente dal cliente originario ed è responsabile nel
trattare in maniera concreta il risultato proveniente dal servitore. Non c'è nessuna interazione, a
livello di principio, push-poll tra il cliente originario e il call-bak object il quale può trattare il
risultato in maniera opportuna; maniera dettata comunque dal cliente prima di richiedere il servizio
suddetto.
Queste due forme sono entrambe molto usate. Guardando i modelli, si vede però che una delle due
forme è fortemente disaccoppiata dalla vita del cliente, naturalmente stiam parlando del call-back
object che potrebbe essere “ancora in vita” anche dopo che il cliente abbia deciso di andare via, a
differenza dell poll-object in cui vi è compresenza tra delegato e cliente.
Naturalmente, per entrambe le forme, l'interazione globale è sincrona non bloccante.
<<Cosa succede se un cliente fa più richieste usando lo stesso poll-object?>>
Bisogna vedere come è fatto il supporto, quindi l'infrastruttura, che potrebbe prevedere una coda,
oppure anche un solo spazio che verrà quindi continuamente sovrascritto dall'ultima richiesta
arrivata.
<<e che garanzie abbiamo che quando andiamo a richiedere il risultato sia proprio quello nostro?>>
Una coda non è sufficiente, bisognerebbe etichettare le richieste. Per questo la maggior parte dei
sistemi che utilizza poll-object lavora con un unico spazio.
Lavorando invece con call-back siamo noi (clienti) che decidiamo come trattare le molteplici
risposte.
43
Reti di Calcolatori LM 9
Prima di fare modelli cliente-servitore sono stati realizzati modelli a scambio di messaggi. Questi,
sono strumenti molto flessibili di basso livello e ce ne sono di moltissimi tipi (socket, …).
A differenza del modello (un) cliente (ed un) servitore, qui ci potrebbero essere più destinatari ed un
solo mittente (broadcast, il messaggio arriva a tutti) oppure gruppi di destinatari (multicast, invio
messaggi differenziato per gruppi di destinatari).
In questo modello potrebbero esserci problemi di semantica: il cliente vuole una certa risposta e
magari gliene arriva un'altra fatta in maniera differente.
Tali modelli, sono stati creati per lavorare in modo ASINCRONO , quindi senza qualità: io inoltro
una richiesta e spero che venga ricevuta. Nonostante ciò, in alcuni sistemi si possono trovare
modelli a scambio di messaggi con interazione sincrona: io mando un messaggio e m 'aspetto un
ack che mi dia, quindi, conferma dell'avvenuta ricevuta del messaggio.
Specifiche semantiche:
Asimmetrico: il mittente conosce il destinatario ma non viceversa.
Simmetrico: il mittente conosce il destinatario e viceversa. Addirittura, ci son dei sistemi in cui il
cliente deve specificare a chi vuole inviare il messaggio e dall'altra parte il destinatario deve
specificare da chi vuole essere inviato dei messaggio. Quest'ultimi sono schemi fortemente
simmetrici, mai affermati in maniera forte perché comporterebbe una completa conoscenza di chi si
vuole che invii dei messaggi e a chi si devono inviare, ciò è estremamente vincolante.
Entità intermedie: con le socket stiamo lavorando in maniera indiretta, anche se sono oggetti locali
abbiam comunque un protocollo che ci permette di trasportare cose dall'altra parte. Nei modelli a
blocco diretto ci son veri e propri oggetti delegati nel ricevere la risposta.
Comunicazione vs Sincronizzazione: nel primo vi è scambio di informazioni, con il secondo invece
si stabiliscono le regole per il trasferimento: non vi è quindi scambio di contenuto.
Possibilità realizzative:
bloccante/non bloccante: aspetto/ non aspetto la risposta del server.
10 Stefano Di Monte
Bufferizzato e non: tipicamente decisione a livello locale implementativo. Bloccante: tengo conto
della memoria dedicata alla risposta e aspetto finché non ci sarà la risposta vera e propria. Non
bloccante: non c'è memoria e il messaggio viene buttato via, quindi senza qualità. Il primo ha un
costo superiore al secondo in quanto occupo una certa memoria per la coda di risposte.
Affidabile e non: abbiamo e non abbiamo una certa garanzia che il messaggio non si perda.
44
Parlando di cliente-servitore ciò che deve sempre succedere è che entrambi siano COMPRESENTI.
Mentre, invece normalmente, nello scambio di messaggi non vi è compresenza e il messaggio viene
mantenuto finché non lo si va a leggere.
Per ciò, il modello c/s è considerato di più alto livello, è più facile da usare. Mentre nel modello a
scambio di messaggi è d'obbligo avere una certa conoscenza di opzioni ascritte alla flessibilità di
tale modello ma che rendono più difficile il suo utilizzo: quindi richiede più conoscenza e
competenza lavorare a scambio di messaggi che non in c/s.
In generale, il modello c/s è più trasparente mentre il meccanismo a scambio di messaggi è più
flessibile e quindi potrebbe avere delle implementazioni più primitive ed efficienti.
Per quanto riguarda i middleware ciò che viene proposto all'utilizzatore è un modello c/s ma ai
livelli più bassi tratteremo di scambio di messaggi, stessa cosa anche quando ad esempio vogliamo
ottenere un interazione con un mittente e molti destinatari.
45
Nel distribuito, quando vogliamo comunicare facciamo transitare del contenuto da qualcuno a
qualcun' altro, con tali entità separate e distinte l'una dall'altro. La comunicazione quindi, impone un
accoppiamento tra le parti, introduce cioè dei vincoli di vario genere sulle entità che interagiscono:
accoppiamento di spazio: le due entità devono comunicare conoscendosi reciprocamente ed essere
co-locate, ossia vicine (nella stessa località geografica).
accoppiamento in tempo: deve esserci un intervallo in cui devono esistere le due entità interagenti.
accoppiamento in sincronizzazione: le due entità citate devono avere delle forme di attesa reciproca
(ma non è detto che ciò sia necessario)
Maggiore è l'accoppiamento maggiore è anche l'insieme di vincoli introdotti dalla comunicazione.
Devo pertanto avere dei casi in cui i sistemi di comunicazione siano caratterizzati da un forte
disaccoppiamento che aumenta l'efficienza del sistema.
D'altra parte però, l'accoppiamento di se per sé, ci fornisce le informazioni necessarie sulla
comunicazione, ci dice in sostanza se la comunicazione sta andando bene o meno, cosa che non
avviene con il disaccoppiamento che non ci permette di conoscere cosa succede dall'altra parte.
46--->48
IL modello ad eventi è considerato molto di alto livello.
É un interazione a 3 entità:
(molteplicità di) produttori: generano gli eventi (publish: pubblicazione dell'evento che fluisce da
Reti di Calcolatori LM 11
questi al supporto).
(molteplicità di) consumatori: interessati agli eventi prodotti (subscribe: manifestazione di interesse
al prodotto di uno o più produttori).
sistema di gestione degli eventi;
E' un modello molti-a-molti, molto flessibile (per questo poco usato perché presenta una semantica
difficile ed una difficile gestione) ed è ottimo, e pensato esclusivamente per il distribuito in quanto
presenta un forte disaccoppiamento (non considera le cose a livello locale anche se in microsoft a
basso livello viene utilizzata una specie di modello ad eventi ma non è ASSOLUTAMENTE la
stessa cosa: quest'ultimo è un modello di gestione di messaggi locali!!)
49
In generale, in fase di implementazione, i modelli ad eventi ci introducono un sacco di problemi
perché abbiam a che fare con molte entità. La parte di supporto, fondamentale per la buona
comunicazione tra le singole parti, bisogna che abbia, ove possibile, un costo limitato, ossia che le
performance siano buone e che la scalabilità del sistema sia elevata (aumentando il numero di utenti
iscritti al gestore degli eventi le performance non si abbassino enormemente: i tempi di latenza siano
limitati), che la qualità dello strumento sia buona, che i produttori e i consumatori siano sviluppabili
in modo disaccoppiato e che da dovunque si possa ottenere il servizio.
50
In prima battuta i sistemi ad eventi, vennero pensati per non avere contenuto informativo, in
sostanza gli eventi erano dei segnali inviati dai produttori con l'effetto di smuovere dei consumatori
interessati. Eventi primitivi come gli interrupt di un interfaccia grafica. Erano, in sintesi, segnali
on/off senza contenuto informativo.
In seconda battuta, venne dato del contenuto a tali eventi al fine di avere della comunicazione e
quindi dello scambio di informazioni, ad esempio gli RSS con registrazione su temi specifici o
multicast differenziato su gruppi di destinatari diversi con registrazioni differenziate.
Sempre di più, con l'utilizzo di sistemi applicativi, si è sentito il bisogno di dare della qualità a tali
eventi, ossia di permettere la ricezione di eventi anche quando si è off-line (eventi persistenti) con
un conseguente forte impatto sulla infrastruttura proprio perché, così facendo, c'è bisogno di
memoria necessaria per il mantenimento di tali eventi.
<<per quanto tempo vengono mantenuti?>> Alcuni, quelli con più qualità, per un tempo illimitato,
altri potrebbero essere eliminati dopo un certo periodo. Tutto ciò perché si ha a che fare con
l'allocazione di risorse e ciò comporta un certo costo.
51
Pertanto, un cliente potrebbe essere interessato solo ad un sottoinsieme di tutto quello che produce il
produttore al quale si è sottoiscritto. E' possibile ottenere ciò filtrando i relativi messaggi sulla base:
• di un topic, quindi basandosi su particolari etichette,
• del contenuto stesso degli eventi prodotti.
• del tipo, quindi sul tipo di messaggio.
52-53-54
Sistemi a tuple
12 Stefano Di Monte
Come sappiamo, per la memoria, leggere e scrivere un dato nella stessa area di memoria può
comportare delle interferenze.
Il sistema a tuple nasce con l'idea di impedire l'interferenza fra azioni di lettura e scrittura su certi
contenuti, nel nostro caso (quindi astraendo) molto di alto livello.
Una tupla è di solito costituita da una serie di attributi che caratterizzano la tupla stessa.
Tutte le tuple debbono essere distinte tra di loro e, in generale, se hanno contenuto, questo dev'essere
differente per ogni tupla. Di conseguenza, non posso aggiungere una tupla che abbia lo stesso
contenuto di un'altra tupla già contenuta nello spazio delle tuple.
In uno spazio di tuple le operazioni che si possono eseguire sono IN e OUT.
Lo spazio delle tuple consente di inserire solo tuple in accordo alla specifica della tupla.
Modello dello SPAZIO delle TUPLE
Come già accennato precedentemente, il disaccoppiamento è alla base di un ottimo sistema
distribuito. Il modello dello spazio delle tuple è caratterizzato da una comunicazione fortemente
persistente e disaccoppiata e sincrona, realizzata attraverso un'entità astratta, lo spazio di tuple per
l'appunto, che è una sorta di memoria condivisa nella quale vengono depositate le tuple e le anti-
tuple. Lo spazio delle tuple è quindi un insieme strutturato di relazioni, intese come contenitori di
attributi e valori per PUB-SUB (le varie tuple). Su uno spazio di tuple si possono depositare ed
estrarre informazioni di alto livello senza causare interferenze; inoltre non ci sono limiti alle tuple
che si possono depositare e che possono rimanere nello spazio delle tuple senza limiti di tempo
(quindi i produttori e i consumatori non hanno la necessità di coesistere nello stesso momento
-disaccoppiamento temporale-) e senza limiti di memoria, la cosa importante è che tali tuple
debbono essere uniche tra loro, ossia tutte diverse.
Su uno spazio di tuple è sempre possibile effettuare operazioni di:
• scrittura/inserimento (in)
• lettura/estrazione (out)
• il match avviene in base al pattern (della richiesta) sui valori degli attributi (in caso di match
con più tuple ne viene estratta una sola in maniera non deterministica), di conseguenza lo
spazio delle tuple dovrà memorizzare non solo quest’ ultime ma anche le richieste in attesa,
quelle cioè che non hanno potuto fare matching con alcuna tupla e che sono in attesa
dell’inserimento di una particolare tupla.
Problema di memoria:
Lo spazio delle tuple potrebbe crescere più della effettiva memoria disponibile.
Soluzione: Partizionamento
Le entità, o nodi, sono organizzate in un anello logico, come mostrato in Figura 1, e ciascun nodo è
caratterizzato da un identificatore univoco. I nodi sono ordinati nell'anello in base al loro
identificatore. La comunicazione si basa sullo scambio di messaggi tra nodi adiacenti e segue un
determinato ordine all'interno dell'anello; in caso di comportamento non deterministico, per ogni
operazione di:
• out viene effettuato un giro dell’anello verificando che in tutto l’anello ci sia una tupla che
soddisfi la richiesta, in caso affermativo la tupla viene estratta e letta; se la tupla non è
presente la richiesta viene inserita in una coda.
• in viene effettuato un giro dell’anello verificando che non ci sia già un’altra richiesta per
quella stessa tupla, in caso affermativo la tupla non viene inserita, in caso contrario la tupla
viene inserita.
Reti di Calcolatori LM 13
N.B.
In caso di comportamento deterministico sia per le operazioni di out che di in non viene effettuato
alcun controllo.
Lo spazio di tuple è distribuito tra tutti i nodi e ognuno ha la responsabilità di mantenere un
sottoinsieme delle risorse formato dalle tuple che hanno identificatore compreso tra quello del nodo
(incluso) e quello del suo predecessore (escluso). La struttura del singolo nodo è mostrata in Figura
2. Esso è composto da differenti moduli organizzati gerarchicamente su più livelli. Ciascun modulo
utilizza le funzionalità offerte da uno o più moduli per portare a termine le attività di cui è
responsabile. La comunicazione tra i moduli avviene sia tramite invocazione di metodi (frecce
continue) sia tramite eventi (frecce tratteggiate).
<<E se dovesse cadere un nodo dell’anello?>>
Replicazione del nodo!
Figura1: Organizzazione dei nodi Figura2: struttura del singolo nodo
55
Abbiamo visto come, molto spesso, nei sistemi tendiamo a delegare qualcuno per fare attuare un
servizio che ci può interessare, in particolare quando abbiamo a che fare con dei riferimenti remoti,
utilizziamo dei proxy, degli intermediari quindi, che potrebbero servire, ad esempio, per trasformare
dati, per il passaggio garantito di certi parametri in modo automatico … . I proxy possono quindi
diventare dei piccoli componenti che ci servono per automatizzare quelle piccole funzioni che
tipicamente sono sempre le stesse.
Ciò che in realtà succede è che gli utenti si vedono diminuire la quantità di compiti assegnatili per la
gestione della comunicazione, compiti che passerebbero quindi a questi intermediari, come ad
esempio, la gestione dei riferimenti remoti (come trovarli, dove cercarli), attivazione del servitore
14 Stefano Di Monte
nel caso in cui questo non sia attivo al momento della comunicazione. Tutto ciò potrebbe essere
effettuato da un supporto (qualcosa di più di un proxy RMI) che è al di sotto, che potremmo
chiamare “broker” o comunque gestore di tutto ciò che c'è di sotto per garantire un corretto aggancio
dinamico tra le entità.
Tornando ad RMI:
<<ci potrebbe essere condivisione del proxy lato cliente tra due clienti diversi, facendo riferimento
allo stesso servitore?>>
Certo! Ma nella maggior parte dei sistemi ciò non avviene, quello che succede è che ogni cliente ha,
per ogni server, quindi per ogni tipo di richiesta, il proprio stub, così come da parte servitore ogni
server ha il proprio skeleton. Come conseguenza, si potrebbe arrivare ad avere (come successe in
microsoft) una marea di proxy difficilmente gestibili!
56
Quindi all'utente è lasciata solo la responsabilità di scrivere della logica di qualcosa, che è usabile
solamente all'interno di un contenitore.
Un contenitore è in sostanza il broker del lucido precedente capace di fare delle azioni ed anche di
eseguirle su richiesta per compito di qualcun' altro ossia del componente, il quale a sua volta vive
dove vi è il contenitore che gli dà le funzionalità necessarie per poter funzionare: si crea quindi un
forte legame ma al tempo stesso uno svincolarsi dal progetto completo.
Quando parliamo di componente, parliamo quindi in generale di qualche cosa a grana più grossa di
un oggetto, capace di fornire delle operazioni attuate da un ambiente di servizio che prende il nome
di contenitore, engine, middleware che fornisce una serie di funzionalità senza le quali un
componente non potrebbe vivere.
In generale i contenitori forniscono una serie di azioni ripetitive sempre uguali che facilmente
possono essere delegate a qualcun' altro (ad esempio l'attivazione del servitore nel caso in cui questo
non sia attivo al momento della comunicazione, o la sua sospensione nel caso in cui esso non venga
interrogato per un certo periodo limitando quindi l'impegno di risorse).
All'utilizzatore rimane il compito di specificare solo la parte contenuta che tipicamente, è di alto
livello, non ripetitiva e fortemente dipendente dalla logica applicativa; pertanto il middleware dovrà
astrarre, per forza di cose, da quest'ultima.
57
Quindi, in prima battuta, potremmo pensare che un container, un engine sia un qualche cosa che
abbia una serie di servizi esposti a dei clienti che ne fanno richiesta. Così dicendo si potrebbe
pensare che normalmente lavori localmente ed attivi e disattivi su bisogno dei componenti locali (gli
unici che potrebbero vederlo).
In realtà, i componenti non sono presenti tutti sulla stessa macchina ma possiamo spesso avere o più
container che si coordinano o un unico container istanziato su varie macchine.
Di container ne esistono diversi perché diverse sono le esigenze, ma la logica resta la medesima:
Reti di Calcolatori LM 15
supportare qualcun' altro.
58
Un contenitore in generale quindi, fornisce delle funzioni di supporto, quelle che ci interessano
maggiormente sono:
• supportano il ciclo di vita dei componenti (un componente vive per tutta la durata
dell'applicazione o vive in base al bisogno previsto, serve comunque mantenere il suo stato
per tutta la sua durata e/o non; attivazione/passivazione … ),
• se siamo dinamici bisogna che ci sia un buon sistema di nomi (pertanto il registry di RMI è
molto limitato) e si parla infatti di discovery (il che aumenta la mia visibilità -ad esempio al
contenuto di altri contenitori-), federazione con altri container... .
• supporto alla qualità del servizio (QoS): deve esistere un accordo in cui il servizio fornito
debba soddisfare dei parametri negoziati tra utente finale ed erogatore,; la mancanza di QoS
potrebbe presentare, in genere, il non pagamento del cliente.
59
container locali: ci interessano poco
60
Con TRASPARENZA si intende, se ci si trova nel distribuito, “nascondere le cose, i dettagli”, il che
va in contrasto con la visibilità.
Trasparenza di:
• Accesso: accedo ad una risorsa in locale ma non m'accorgo che è locale.
• Allocazione: non mi accorgo che in realtà la risorsa è allocata da un'altra parte, quindi
quando creo assumo che stia creando sullo stesso mio nodo.
• Nome: identifico con nomi tutti uguali le risorse locali e remote.
• Esecuzione: non mi accorgo se la mia esecuzione sta impegnando risorse locali o remote
• Performance: non mi accorgo, richiedendo un servizio, che questo sia remoto o locale,
avendo quindi una differenza di performance: per tale motivo tale tipo di trasparenza è
difficile da avere perché è difficile non accorgersi del differente livello di performance.
• Fault: capacità di fornire il servizio anche in caso di guasti ---> replicazione.
Più un sistema è grande e meno c'è trasparenza!
Nonostante ciò, la trasparenza è un requisito sempre da cercare!
Fatto sta comunque che sistemi totalmente trasparenti non si sono mai realizzati perché non
avrebbero mai avuto mercato, ciò renderebbe impossibile servizi location-awereness per la
16 Stefano Di Monte
generazioni di servizi dipendenti dalla località di chi richiede il servizio stesso, o anche non
permetterebbe a quei clienti un po' più avanzati di poter vedere dettagli....per tale motivo si parla di
“traslucidità”: chi vuole vedere ciò che c'è sotto lo può fare altrimenti è tutto trasparente.
61 - 62
Negli anni 2000, alcune delle più importanti società americane e internazionali, han deciso che
valeva la pena di cominciare a ridefinire i servizi da fornire agli utenti finali. Lo standard definito è
TINA, nuova architettura di rete per un supporto a uso informatico, descritta da una manualistica
più limitata di quella di OSI ma con delle direzioni molto precise:
innanzitutto non è un modello c/s, è più coordinato, anche se rapporti c/s non mancano, ma questi
non rappresentano l'intera architettura. Ragionando per un cliente solo, l'idea di base è che questi
abbia bisogno di servizi di comunicazione, di fornire quindi banda, ma ciò non basta.
Tale idea viene affiancata da un idea molto precisa di fornire dei servizi su quella banda (provider
di rete (che devono federarsi) + provider di servizi).
In conclusione, tale modello, è molto coordinato, sicuramente con rapporti c/s singoli, ma che è, nel
complesso, più articolato.
Molto importante è l'idea di gestione dei servizi: non si pensa solo a fornire i servizi, ma anche a
controllarli! Da ciò ne deriva una certa qualità del servizio stesso, dipendente da più fattori, quindi
da più provider (di rete e di servizio).
Modelli abbastanza astratti:
63
RAM = Random Access Machine (assomiglia ad una macchina di Von Neuman)
E' una macchina costituita da:
• un programma, quindi una sequenza di istruzioni contenute in determinate locazioni, quindi
di una certa dimensione. Il programma è assolutamente inalterabile.
• una memoria, acceduta in lettura e scrittura dal programma, con celle tutte della stessa
dimensione predeterminate.
• un solo accumulatore, con dimensione anche uguale a quella della memoria
• un nastro di INput
• un nastro di OUTput
Le RAM sono state molto usate per fare della previsione di quello che succede nel distribuito.
Hanno una modalità di indirizzamento vario (indiretto, diretto ecc) ma vi è un vincolo: le istruzioni
Reti di Calcolatori LM 17
eseguono tutte nello stesso tempo (ipotesi molto riduttiva e per ciò, da questo punto di vista, è
quindi una macchina astratta). Tale macchina esegue in sequenza il programma, il quale non ha un
limite di dimensione (cosa astratta) così come per la memoria (entrambe le due cose però son decise
staticamente).
65
PRAM (Parallel RAM)
E' un estensione della RAM.
É composta da:
• una serie di P programmi, tutti diversi tra loro e con estensioni varie con ciascun un
accumulatore differente , ma che condividono tutti la stessa memoria.
• una serie di P accumulatori
• un nastro di input
• un nastro di output
• un unica memoria condivisa (da ciò modello globale) (quindi le locazioni di memoria
possono essere viste da tutti allo stesso modo), che può quindi anche servire per trasferire
informazioni da un programma ad un altro, con possibilità di interferenze.
In una PRAM, i programmi partono dalla prima istruzione, tutti insieme, per poi eseguire tutti
insieme la seconda istruzione, poi la terza (sempre tutti insiemi) e così via: la durata dell'istruzione è
la stessa per tutti programmi; ciò non vuol dire che tutti i programmi terminino tutti insieme perché
potrebbero essere (e lo sono) tutti di dimensioni differenti.
É un modello MIMD fortemente sincrono, in un cui le istruzioni sono eseguite da tutti, una alla
volta, in modo sincrono (e ciò non è una cosa banale!).
66
Per definire meglio la semantica di lavoro bisogna che capiamo cosa succede quando più programmi
18 Stefano Di Monte
accedano alla stessa locazione di memoria, per ciò le operazioni di lettura e scrittura sono:
• o sequenzializzate: due programmi accedono alla stessa locazione di memoria, uno aspetta e
l'altro esegue (ad esempio EREW-ERCW), in sostanza in mutua esclusione.
• o concorrenziale/simultaneo: più p RAM della PRAM (anche tutte) accedono in modo
concorrenziale alla memoria (ad esempio CREW-CRCW).
67
Le operazioni concorrenti funzionano sempre bene (vale solo l'ultimo valore scritto che sarà sempre
ben fatto e non spurio) a differenza del sequenziale, non si parla ancora di interferenza, ma ciò che
succede è che ogni programma scrive ad un ciclo di istruzione comportando un certo numero di
ritardi magari eccessivo se P è grande.
Tale modello presenta forti ipotesi riduttive che non si fanno nel modo reale:
1. eseguire delle istruzioni in modo sincrono è abbastanza complicato da realizzare con
l'aumentare di P.
2. all'aumentare di P non è neanche banale avere un accesso condiviso alla memoria (cosa che
si fa solo per un numero di accessi limitato).
Per ciò, tale modello globale è di difficile realizzazione. Da notare inoltre che c'è una fortissima
condivisione dell'input e dell'output, pertanto il tempo delle operazioni di input/output è trascurato e
l'accesso ad un unico nastro non cresce con l'aumentare di P.
68
MPRAM (MessagePassing RAM)
E' un modello un po' meno condiviso rispetto a PRAM.
É composto da:
• Elimina la memoria globale, ogni programma ha una sua memoria privata (quindi se P
programmi allora P memorie)
• ha P accumulatori,
• un nastro di IN
• ed un nastro di OUT.
Ogni programma potrebbe essere messo in comunicazione con l'altro attraverso dei canali di
comunicazione, i quali dicono che ogni nodo ha un certo numero di vicini: se fossero tre
programmi, allora sarebbero tutti collegati tra di loro ed in quest'ultimo caso, quello che si creerebbe
è un anello.
Reti di Calcolatori LM 19
Con P programmi abbiamo un numero di canali pari a: num_canali = (P*(P-1))/2.
NOTA: i canali sono bidirezionali.
69
Le operazioni nei canali sono delle receive e delle send.
Nelle MPRAM, la semantica è una semantica rendez-vous esteso, ossia chi prima arriva aspetta
l'altro: se faccio una send aspetto che dall'altra parte l'altro faccia match, quindi se devo inviare a
molti: invio al primo ed aspetto che faccia match, poi posso inviare al secondo e aspetto che anche
questo faccia match e così via.
Questo è un modello locale perché non vi è niente di condiviso, non vi è niente di globalmente
accessibile a tutti.
70
L'espressività di un modello globale è molto più larga che quella di un modello locale: le azioni
facili da fare in un modello globale sono più difficili da realizzare in un modello locale.
Difatti in un modello globale è molto più semplice fare del broadcast. In particolare per una
istruzione di broadcast, in un modello MPRAM ogni nodo intermediario deve fare una send ed una
receive, deve fare quindi del routing con le istruzioni che si diffondono punto-punto (realizzando in
tal modo una specie di memoria comune); in un modello PRAM tutto si risolve con un'istruzione (la
capacità espressiva è la stessa ma, ovviamente, l'espressività dei modelli è completamente differente
in termini di quantità).
D'altro canto, in un modello PRAM non possiamo aumentare la memoria condivisa, mentre invece
in un modello MPRAM è possibile aumentare il numero di programmi e quindi di canali.
71
Con le PRAM e le MPRAM si sono realizzato molti teoremi.
In particolare, con le PRAM sono stati creati molti teoremi a memoria condivisa.
Con le MPRAM, è stato creato il teorema del routing ottimo: per andare da un nodo X ad un nodo
Y, X manda un messaggio in modo random a K che a sua volta manda un messaggio random a Y.
<<Cosa vuol dire fare del routing ottimo?>>
Vuol dire, fare del routing che garantisca al meglio la distribuzione del traffico su tutti i possibili
canali. Di conseguenza lavorare random è la cosa migliore, perché se per caso avessi molto traffico
tra X e Y, tale canale, lavorando normalmente, si ingolferebbe sicuramente.
Così facendo sollecitiamo tutti i canali in modo abbastanza bilanciato, cosa che, però, non è mai
20 Stefano Di Monte
vera nella realtà.
72
Quello che si fa quindi, è non innamorarsi troppo del modello, perché poi potrebbe essere difficile
rappresentarlo fedelmente nella realtà!
Utilizziamo quindi degli indicatori che ci forniscano delle stime di cosa ci può dare quel
determinato modello.
Indicatori che saranno richiamati da degli algoritmi dipendenti dalla dimensione N del problema,
con N che può crescere infinitamente.
Tipicamente quando abbiamo un problema, ragionando in base agli indicatori, avremo poi una
soluzione in termini di complessità di tempo T(N), ossia quanto tempo ci mette quell' algoritmo a
risolvere quel problema; altro indicatore utile è la complessità in termini di spazio CS(n) (che noi
utilizzeremo meno).
Parlando sempre di distribuito, potremmo lavorare con un unico processore, soluzione sequenziale
con T1(N), oppure con P processori, soluzione parallela con Tp(N); in generale quindi, l'aggiunta di
processori tende a far diminuire il tempo di esecuzione, quindi Tp(N) < T1(N).
73
Ci sono due indicatori fondamentali che sono la base di tutta la teoria dell'ottimizzazione e sono:
• Lo speed-up è la misura di quanto miglioriamo con l'introduzione di risorse, in particolare si
indica con S(P, N) ed è uguale a
S(P,N)= T(1,N) / T(P,N)
quello che ci aspettiamo è che tale valore sia >1, se così non fosse opteremo quindi per la soluzione
sequenziale.
Nota:
lo speed-up “possibile” è: 0<=speed-up<=P.
lo speed-up “ragionevole”: 1<=speed-up<=P. (Se =1 non abbiamo nessuna miglioria!)
Pertanto, se riuscissimo a spezzare il programma in sottoparti indipendenti tra loro, lo speed-up
totale sarà quindi sicuramente P in quanto, ad esempio, se avessimo 4 processori che si dividono un
quarto dell'applicazione,ognuno ci metterà quindi ¼ del tempo ed ovviamente lo speed-up sarà 4!
• L'efficienza è lo “speed-up” diviso “il numero di processori utilizzati”, ci dice quindi quanto
bene lavorano i processori (se abbiam che tutti i processori lavorano contemporaneamente
allora l'efficienza sarà massima, altrimenti man mano che abbiamo processori idle,
l'efficienza calerà in base a quanti saranno quest'ultimi: se non lavora nessuno l'efficienza è
minima, quindi uguale a 0).
Nota:
l'efficienza è un valore normalizzato: 0<=eff<=1. (quindi se è 0, siam messi male perché abbiamo
introdotto dei processori ma guadagniamo molto poco), rappresenta lo speed-up normalizzato.
74
Tipicamente se lavoriamo in un sistema ideale, lo speed-up è I.
Nota: guardando il grafico, la parte sopra la retta non è possibile.
Reti di Calcolatori LM 21
Cercando una soluzione in un albero quello che si fa tipicamente in sequenziale è cercarla in
profondità esplorando magari tutti i sotto alberi e se la soluzione fosse alla fine dell'ultimo
sottoalbero con costo di esecuzione relativamente alto. Lavorando in parallelo il costo della stessa
esecuzione sarà molto molto minore di quella in sequenziale.
75
legge di grosh:
“Il migliore sviluppo (deployment) per un programma è una esecuzione sequenzializzata su un
unico processore”
Ciò si scontra con il distribuito, dove abbiamo molte risorse e molti vincoli che non permettono di
stare su un unico processore (quest'ultima è soluzione sempre adottata se fattibile).
I due indicatori precedentemente enunciati, speed-up ed efficienza, dipendono da N (dimensione del
problema) e P (numero di processori, grado di parallelizzazione), fattori che possono essere
esaminati in modi separati, indipendenti o dipendenti tra loro:
• Fattore di carico (loading factor): L= N/P
Distribuendo la complessità del problema in modo omogeneo fra i diversi processori otteniamo
l'idea del carico del singolo processore.
I valori N e P sono stati studiati dai sistemi lavorando in:
identity size: N e P hanno lo stesso valore (N == P)
independent size: si stabilisce che N è indipendente da P
dependent size: molto spesso si lavora con N dipendente da P
Ciò che vogliamo quindi è capire qual'è il miglior modo di lavorare!
76-77
legge di Amdhal:
“Se abbiamo un programma lo speed-up che si può ottenere è limitato dalla parte sequenziale”
Nella realtà, il problema dà dei vincoli allo speed-up, vincoli dati dalla parte sequenziale del
problema stesso e non da quella parallela, che limitano lo speed-up.
Anche se il numero dei processori in parallelo fosse infinito, al di sopra di una certa soglia (vedi
slide 77) lo speed-up non può andare: limite imposto dalla parte sequenziale!
22 Stefano Di Monte
NOTA; tutto si svolge al di sotto della linea nera e di quella rossa.
Quindi non vale la pena introdurre un numero alto di processori bensì vale la pena introdurre un
numero di processori pari a quello che realmente serve, perché aumentando il numero di processori
diminuisce l'efficienza (i processori in eccesso lavorano meno e non al pieno delle loro possibilità).
78
Heavily Loaded Limit: THL(N) = infP TP(N)
ciò vuol dire che la complessità in N viene stabilita studiando il sistema al variare di P (in maniera
crescente) al fine di trovare quel valore di P che ci dà complessità minima.
Come suggerisce il nome stesso Heavily Loaded (molto caricato), il valore massimo dello speed-up,
minimo quindi della complessità in tempo parallela, si ottiene caricando molto il processore!
Quindi si lavora bene se il fattore di carico è elevato.
Nota:
La complessità in tempo potrebbe essere divisa in due parti: la parte di computazione TCompP e quella
di comunicazione TCommP (ad esempio tra due canali), pertanto:
TP(N) = TcompP + TCommP
A sua volta la parte di computazione si divide in una parte parallelizzabile TCompPar ed una parte
limitante sequenziale TcompSeq, ossia:
TCompP = TCompPar + TcompSeq
79
esempio: somma di N numeri.
• complessità nel sequenziale: O(N).
• complessità nel parallelo:
immaginiamo un albero binario completo:
Reti di Calcolatori LM 23
ogni nodo è un processore, quindi il numero totale di valori e all'incirca uguale a quello dei
processori P , ed è: P= 2H+1 – 1 con H=numero di livelli.
Lavoriamo in identity size (N == P): il numero dei nodi è uguale al numero dei valori da sommare
(in realtà quasi uguali -per il meno1 della formula per il totale dei cpu-).
I valori da sommare vengono passati dalle foglie, sommati e passati da processore in processore
(che a loro volta sommano i dati) attraverso i rami dell'albero fino alla radice che restituirà il
risultato.
Complessità dell'algoritmo:
TP(N) = O(H) = O(log2(N)) = ~ 2 * log2(N)
(il 2 davanti sta perché due sono le comunicazione verso il nodo padre).
Si noti che la complessità dell'algoritmo è misurata dipendentemente dal numero dei livelli:
H = O(log2(P)) = O(log2(N))
che se elevato ha effetti negativi sulla complessità.
80
Al crescere di N lo speed-up va su, non arriva a P ma comunque si alza.
Per quanto riguarda l'efficienza questa sarà uguale a 1/log2(N), che al crescere di N fa diminuire
l'efficienza che tende a zero. Difatti partendo dalle foglie alla radice, i processori idle aumentano
costantemente, quando siamo alla radice l'efficienza sarà quindi minima perché quasi la totalità dei
processori sarà idle, sarà con le mani in mano perché ha già eseguito il suo compito.
24 Stefano Di Monte
Perciò nel mondo ideale questa è una soluzione che non si adotta mai, anche perché con un numero
di valori elevato sarebbe necessario un numero altrettanto elevato di processori: soluzione
IANACCETTABILE!.
81 – 82 -83
Con N molto maggiore di P stiamo quindi partizionando i nodi, i processori, i quali hanno un lavoro
locale da fare ed una fase di scambio di comunicazione per combinare i dati.
La complessità in tempo, di tale soluzione, è la somma di questi due fattori (lavoro locale + fase di
comunicazione).
Lo speed-up e l'efficienza dipenderanno conseguenzialmente,anche loro, da questi due fattori.
Di conseguenza, lo speed-up tende a P mentre l'efficienza tende a 1.
Concludendo, tanto più i nodi sono caricati tanto meglio lavoriamo in termini di speed-up ed
efficienza.
84 - 85
Quest'ultimo modello è assolutamente un modello ideale, ciò che avviene nel mondo reale non è
questo.
Ad esempio, ciò che abbiamo supposto è che i dati salgono su verso la radice in modo veloce senza
alcun disturbo, supponendo che invece per una qualche ragione un nodo abbia altre cose da fare e la
comunicazione venga ritardata, ciò comporterebbe un effetto di svantaggio generale sul tempo totale
di esecuzione.
Ci sono un sacco di possibilità di peggiorare la situazione “ideale”.
Maggiore è il numero di nodi maggiore è il rischio di avere colli di bottiglia molto pronunciati a
differenza, ad esempio, di un sistema con 10 nodi.
In generale, quando si parla di speed-up ed efficienza si fa sempre riferimento alla media, ma in
alcuni casi ci potrebbe interessare il caso peggiore.
La realtà è completamente diversa dal caso ideale, e ciò è dovuto alla presenza di molti fattori; per
tale motivo gli indicatori di speed-up ed efficienza sono solo il primo passo.
Di sicuro comunque, ragioniamo sempre sul sistema quando questo è a regime, quando cioè è tutto
pronto, tenendo in considerazione che la comunicazione comporta sincronizzazione e quindi ritardo,
peggiorando inevitabilmente le performance.
86
Overhead
Come dev'essere fatto il mappaggio non è tenuto in conto in nessun modo da speed-up ed efficienza,
questi ci danno delle indicazioni di massima, ideali.
Pertanto, un indicatore molto importante è l'indicatore di overhead che tiene in conto le risorse e il
Reti di Calcolatori LM 25
tempo speso in comunicazione.
Supponendo di essere in un'applicazione ideale, ossia in una applicazione infinitamente
parallelizzabile (ovvero che può essere infinitamente divisa in più parti ed ogni parte associata ad
un processore che lavora solo su questa parte ed è indipendente da tutti gli altri processori quindi
senza overhead -in realtà questi hanno bisogno di coordinarsi...-), introdurre un certo numero di
processori ci porta ad introdurre un risparmio che è pari al numero di processori utilizzati; nella
realtà invece non è così: ciò che risulta è che
P*TP(N) > T1(N)
e tanto è maggiore tale prodotto, tanto è maggiore quello che perdiamo rispetto al caso ideale.
In altre parole, se idealmente l' overhead è nullo, quindi T0 = 0 perché P*TP(N) = T1(N), realmente
invece succede che T0 > 0 perché P*TP(N) > T1(N).
Ovviamente non si possono misurare tutte le fonti di overhead (comunicazione, trasferimento
iniziale dei dati, stampa e gestione valori intermedi, raccolta dei risultati...), così ciò che viene in
mente è misurare l' overhead vedendo quanto è maggiore la differenza della complessità in termini
di tempo tra il sequenziale e il parallelo, pertanto il tempo di overhead T0 si misura come:
T0 = |P*TP(N) - 1*T1(N)|
In sostanza diciamo che, se in un caso ideale dovremmo avere una situazione in cui, introducendo
un processore otteniamo un miglioramento sullo speed-up ideale di P, siccome lavoriamo in un
modo reale, questi due fattori non sono uguali e quindi andiamo a considerarne la differenza ossia il
tempo di overhead.
Il tempo di overhead stima tutti i fattori non ideali (comunicazione, tempo di configurazione, tempo
di I/O …) ed è dipendente da P, ossia dal numero di processori. Difatti maggiore è P, maggiore
conseguentemente è anche l' overhead.
87
Nei sistemi veri T0 è difficilmente =0. Giacché non è uguale a 0, possiamo usarlo per esprimere TP
in termini di tempo sequenziale e tempo di overhead.
Quindi a questo punto possiamo calcolare speed-up ed efficienza, le quali dipenderanno
sicuramente da T0.
26 Stefano Di Monte
Come si nota per quanto riguarda l'efficienza: minore è l' overhead più vicino a 1 è l'efficienza
stessa.
88 - 89
caso di studio precedente con P<<N ed in cui si nota che T0 è fortemente dipendente da P: maggiore
è il numero di processori maggiore è l' overhead.
90
Isoeffcienza
L'obiettivo è mantenere costante l'efficienza, Pertanto partendo dalla formula dell'efficienza in
termini di overhead, si ottiene:
Definendo K=((1-E/E)), si vede chiaramente che il tempo di overhead dipende da un fattore K che
consente di ricavarlo a partire da T1.
Tutto questo ragionamento va sotto il nome di sistema isoefficiente: ossia che hanno una efficienza
costante, quindi dei sistemi scalabili (dei sistemi praticamente ideali), sempre efficienti al variare
del numero dei processori.
K è una costante, costante di efficienza, nei sistemi che s'avvicinano molto al caso ideale.
91
Quello che noi vorremmo è quindi che un sistema sia ad efficienza costante e quindi, caratterizzato
da questa costante K.
Se K è piccola, le cose vanno bene perché introduciamo pochi effetti di perdita di risorse
utili (alta scalabilità).
Se K è elevata, il sistema è meno scalabile.
In realtà, nei sistemi reali, K non è mai una costante ed è sempre in funzione del numero dei
processori. Perciò nella realtà, un sistema reale è sempre scarsamente scalabile!
92
<<Data un applicazione con Q processi (Q inteso >>), ed infiniti processori a disposizione, come
gestire l'allocazione dei processori?>>
Sicuramente un numero pari a 1<<P<<Q/2, in quanto è sempre meglio mantenere carichi i
processori: così facendo il mio sistema avrebbe un alta efficienza.
Reti di Calcolatori LM 27
28 Stefano Di Monte