![]() | ![]() |
|
|
LA SEND TIPO 'CHIAMATA A PROCEDURA REMOTA'
Prendiamo in esame il terzo ed ultimo tipo di send: la CHIAMATA A PROCEDURA REMOTA, una send a livello di linguaggio, detta anche rendez vous esteso.
In questo caso il mittente non soltanto rimane in attesa che il destinatario riceva il messaggio, ma aspetta anche un messaggio di risposta dal destinatario. In altre parole, il mittente (che viene detto anche cliente) chiede un servizio al destinatario(servitore): gli passa dei parametri con i quali specifica il tipo di servizio desid 656d35g erato, e dei parametri di ritorno destinati a contenere i risultati. I risultati del servizio richiesto possono consistere in una semplice risposta, la quale dice in sostanza se il servizio è stato realizzato o meno, oppure possono essere delle informazioni più articolate.
In generale, dunque, i tempi di attesa saranno più lunghi rispetto a quelli della send sincrona, se è vero che il mittente deve rimanere bloccato per tutto il tempo che il ricevente impiega ad eseguire il servizio richiesto e a restituire i risultati.
A basso livello, una simile send può essere realizzata mediante una send asincrona e una receive bloccante, entrambe eseguite all'interno del cliente. In questo modo il cliente spedisce il messaggio senza bloccarsi in fase di invio, e poi si mette in attesa sulla ricezione del risultato. La 'chiamata a procedura remota', un costrutto proprio dei linguaggi di programmazione concorrente ad alto livello, ha la stessa struttura di una normale chiamata a sottoprogramma, cioè:
Call
Pippo (parametri) Send
Pippo (parametri)
La similitudine con le chiamate a sottoprogramma arriva al punto che il cliente che utilizza questo costrutto desidera avere completa visibilità del servizio richiesto, usufruendone come se il servitore fosse un suo sottoprogramma, mentre ovviamente esso 'gira' in un differente contesto se non addirittura su di un'altra macchina. Per realizzare questa trasparenza si pone un problema di passaggio dei parametri.
Se ad esempio il cliente passa dei parametri di ingresso per indirizzo, questi devono essere dapprima trasformati in dati veri e propri dal kernel, e poi spediti. Si inizia con una fase di 'impacchettamento' del messaggio da parte del cliente, che memorizza i parametri di scambio inopportune locazione della propria area di memoria privata. Il cliente esegue quindi la sequenza send asincrona - receive bloccante. Ricevuto il segnale di 'ok' da parte del servitore, il compilatore concorrente ricostruisce a partire dai parametri di scambio il messaggio da inviare a quest'ultimo mediante un'operazione di 'spacchettamento' a suo favore.
Corrispondentemente, il servitore prevederà nell'ordine le seguenti operazioni:
- una receive bloccante (questa è sempre la prima istruzione del servitore, e quindi è implicito che in questo caso il destinatario resta immobile fino a che qualcuno non richiede un suo servizio);
- uno 'spacchettamento' del messaggio in arrivo da parte del kernel;
- l'esecuzione del servizio;
- un 'rimpacchettamento' del risultato, nelle locazione di memoria dell'area privata del servitore;
- una send asincrona.
PRODUTTORI CONSUMATORI
NEL MODELLO A SCAMBIO DEI MESSAGGI
Risolveremo questo problema mediante i costrutti send asincrona e receive bloccante.
In questo caso, abbiamo bisogno di un PROCESSO BUFFER, dedicato ad assolvere quelle funzioni che nel modello a memoria comune venivano svolte dal buffer di memoria condiviso tra produttori e consumatori. Ciò è necessario, perché nel modello a memoria locale non è più possibile parlare di risorse condivise (per lo meno non nell'accezione a noi nota) e quindi non si può fare ricorso ad un buffer di memoria condiviso. Si deve allora prendere un buffer di memoria e renderlo 'privato' ad un processo, che rappresenterà il gestore di tale risorsa; produttori & consumatori allora 'dialogheranno' con tale gestore per poter effettuare lo scambio dei messaggi. Lo schema di riferimento è quello riportato nella pagina seguente.
Le primitive usate sono:
SEND (mes, proc)
dove mes = variabile 'messaggio inviato', appartenente ad un certo tipo che indichiamo con T, e proc = nome del processo destinatario, appartenete al tipo che conosciamo col nome di process;
Proc : = RECEIVE (mes)
Mes sarà naturalmente una variabile dello stesso tipo. Si noti inoltre che la primitiva RECEIVE restituisce come parametro il nome del processo mittente e questo nome viene assegnato alla variabile proc (informazione a beneficio del destinatario).
Si suppone che ogni processo in grado di ricevere dei messaggi possegga una coda di messaggi a lui inviati che viene gestita dal kernel (queste code non vanno confuse con le code locali del processo buffer, di cui poi si dirà). La SEND, ogni volta che viene chiamata, inserisce il messaggio mes nella coda di ingresso del processo proc e termina. RECEIVE analizza la coda di ingresso del processo che la esegue: se è vuota, il processo viene bloccato, altrimenti viene estratto il primo messaggio e il suo valore viene assegnato alla variabile mes.
produttore
1
consumatore 1
PRONTO
produttore
i
consumatore i
DATI
DATI
produttore
n consumatore
n
Riportiamo di seguito il codice: innanzitutto, il testo del processo buffer (spiegazioni dettagliate saranno fornite subito dopo).
Process buffer ;
var coda_dati: < coda di elementi di tipo T > ;
coda_consumatori _pronti: < coda di nomi di processo > ;
inf: T ;
consumatore, proc: process;
in: in_mess;
out: out_mess;
begin
while true do
begin
proc : = RECEIVE (in) ;
case in.specie of
dati
begin
if < coda_consumatori_pronti vuota > then
< inserzione di in.informazione nella coda_dati > ;
else
begin
< estrazione di consumatore da coda_consumatori_pronti > ;
out : = in.informazione ;
SEND (out, consumatore) ;
end ;
end ;
pronto:
begin
if < coda_dati vuota > then
< inserzione proc in coda_consumatori_pronti > ;
else
begin
< estrazione di inf da coda_dati > ;
out : = inf ;
SEND (out, proc) ;
end ;
end ;
end ;
end ;
Sono stati usati in particolare i seguenti tre tipi:
type tipo_messaggio_ricevuto = (dati, pronto) ;
type in_mess = record
case specie: tipo_messaggio ricevuto of
dati: (informazione: T) ;
pronto: () ;
end ;
type out_mess = T ;
Il testo dei processi produttori e consumatori è quello che segue:
Process produttore_i ;
var mess: in_mess ;
contenuto : T ;
begin
mess.specie : = dati ;
while true do
begin
< produci contenuto > ;
mess.informazione : = contenuto ;
SEND (mess, buffer) ;
end ;
end ;
Process consumatore_j ;
var mess1: in_mess ;
mess2: out_mess ;
proc: process ;
begin
mess1.specie : = pronto ;
while true do
SEND (mess1, buffer) ;
proc : = RECEIVE (mess2) ;
< consuma mess2 > ;
end ;
end ;
Ogni consumatore notifica a buffer la sua disponibilità a ricevere dati inviandogli il messaggio di controllo pronto. Ogni produttore invia viceversa a buffer dei dati. Buffer dal canto suo svolge un lavoro di interfacciamento, inviando ai consumatori i dati in arrivo dai produttori. (cfr. schema pag.115).
I messaggi ricevuti da buffer, inviati sia dai produttori che dai consumatori, appartengono al tipo in_mess. Per discriminare fra i due casi, si usa il campo specie il quale indica se il messaggio appena ricevuto è stato spedito da un produttore (in.specie = dati) o da un consumatore (in.specie = pronto). Nel primo caso potrà essere utilizzato un secondo campo, informazione (si tratta del ben noto costrutto 'record con varianti').
I messaggi inviati da buffer ai consumatori appartengono al tipo out_mess.
Si noti che il processo BUFFER è un servitore (cfr. pag.107) e come tale rimane sospeso (sull'istruzione proc := Receive (in)) finché nella coda kernel dei messaggi in arrivo al buffer (la quale contiene sia dati che messaggi di pronta ricezione) non c'è almeno un elemento. Subito dopo la chiamata, la variabile proc conterrà l'informazione 'nome del processo chiamante'. La successiva elaborazione viene fatta come segue sulla base della natura del messaggio pervenuto.
Se il messaggio proviene da un produttore (e quindi si tratta di dati), si provvede all'invio solo se ci è almeno un consumatore in attesa. Diversamente, esso viene memorizzato da buffer in una coda locale, CODA_DATI, in cui vengono inseriti tutti i dati ricevuti dai produttori e non ancora inviati ai consumatori. Se il messaggio proviene da un consumatore (e quindi è un segnale pronto), si controlla la coda locale, e se questa non è vuota si preleva un messaggio e si procede al suo invio. Se la coda è vuota il nome del consumatore viene memorizzato in una coda locale, CODA_CONSUMATORI PRONTI, contenente i nomi dei consumatori in attesa di dati [2].
Questa soluzione presenta un evidente problema di spazio di memoria. La coda del kernel assegnata al processo buffer potrebbe saturarsi rapidamente; infatti i processi potrebbero effettuare delle send ad alta velocità in un momento in cui il buffer non è in condizione di ricevere i loro messaggi (perché non gli viene assegnata la CPU). Analogamente le code locali di buffer possono facilmente saturarsi, se i produttori inviano molti messaggi di dati che i consumatori tardano a rilevare, o se i consumatori inviano molti più messaggi di pronta ricezione di quanti non siano i messaggi di dati prodotti.
Uno dei modi di superare questa difficoltà consiste nell'usare le primitive sincrone. La loro sintassi è la stessa del caso precedente.
SEND (mess, proc);
proc : = RECEIVE (mess) ;
Questo è lo schema relativo alla soluzione che viene presentata a seguire. Esso fa riferimento al semplice caso di un solo produttore e un solo consumatore.
"richiesta di servizio" "richiesta di servizio"
Pronto Pronto
Produttore Buffer Consumatore
'cliente' 'servitore' 'cliente'
dati dati
"servizio" "servizio"
Il codice:
Process buffer ;
var coda: < coda di elementi di tipo T > ;
proc: process;
cons_pronto, prod_pronto: boolean initial false ;
dati: T ;
pronto: signal ;
begin
while true do
begin
proc : = RECEIVE (pronto) ;
case proc of
produttore :
if < coda piena > then
prod_pronto : = true ;
else
begin
proc : = RECEIVE (dati) ;
if cons_pronto then
begin
SEND (dati, consumatore) ;
cons_pronto : = false ;
end ;
else < inserzione dati in coda > ;
end ;
consumatore :
if < coda vuota > then
cons_pronto : = true ;
else
begin
< estrazione dati da coda > ;
SEND (dati, consumatore) ;
if prod_pronto then /* si è liberato un posto nella coda */
begin
proc : = RECEIVE (dati) ;
< inserzione dati in coda > ;
prod_pronto : = false ;
end :
end ;
end ;
end ;
I processi produttore e consumatore aderiscono al seguente schema:
Process produttore ;
var dati: T ;
pronto: signal ;
begin
while true do
begin
< produci dati > ;
SEND (pronto, buffer) ;
SEND (dati, buffer) ;
end ;
end ;
process consumatore ;
var dati: T ;
pronto: signal ;
proc: process;
begin
while true do
begin
SEND (pronto, buffer) ;
proc : = RECEIVE (dati) ;
< consuma dati > ;
end ;
end ;
Tutte le SEND utilizzate sono sincrone e quindi bloccano il processo che le esegue fino alla corrispondente esecuzione di una RECEIVE del processo destinatario. Si osservi che questa soluzione presenta una notevole omogeneità; grazie alla sincronizzazione implicita nella SEND, il protocollo eseguito dal produttore è equivalente a quello eseguito dal consumatore dando luogo così ad un comportamento complessivo simmetrico: entrambi i processi coinvolti sono clienti che chiedono un servizio a buffer (cfr. figura pag. 119).
Prod_pronto e cons_pronto hanno lo scopo di memorizzare la ricezione di un segnale 'pronto' da parte del buffer quando questo si trova in uno stato in cui non può soddisfare istantaneamente una richiesta. Poiché abbiamo a che fare con due soli processi, uno che produce, l'altro che consuma, sono sufficienti queste due variabili booleane; contrariamente, avremmo dovuto usare delle strutture dati più complesse.
Per poter inviare dei dati che ha generato, il produttore deve realizzare in sequenza queste operazioni:
1) invio di un segnale di disponibilità, pronto, a spedire dei dati;
2) attesa del segnale di 'ok to send' da parte di buffer; tale segnale viene ritardato nel caso in cui la coda a disposizione risulti piena: bisogna aspettare che il consumatore liberi uno degli 'scomparti' della coda [3];
3) invio vero e proprio dei dati (si noti che questa send non comporta un ritardo se non la normale attesa implicita nella send sincrona, e dunque è possibile che il produttore spedisca a più riprese dei dati fino a saturare la coda). Se c'è il consumatore ad attendere, i dati vengono subito consumati, altrimenti vengono posti nella coda.
Dal canto suo, il consumatore si limita a spedire il segnale pronto di disponibilità a ricevere dei dati, subendo un ritardo nel caso in cui la coda dei dati sia vuota. Non appena la richiesta viene soddisfatta, occorre controllare se il produttore era stato sospeso in fase di invio dati (perché aveva trovato la coda piena). In caso affermativo, i suoi dati possono essere sistemati nella coda, poiché questa ha ora un posto libero.
Contrariamente a quanto avviene per buffer, non sembrerebbe necessario provvedere al generico processo consumatore una coda, gestita dal kernel, destinata a contenere i messaggi in arrivo. L'istruzione Send (out, consumatore) del processo buffer non avrebbe motivo di inserire il messaggio in una coda di ingressi, dato che il processo consumatore non chiede mai più di un messaggio per volta (ogni send è seguita da una receive) e non è previsto che vengano inviati messaggi dei quali non si è fatta richiesta; sennonché (Ancilotti-Boari, pag.187): "La primitiva SEND asincrona prevede già, a livello di supporto a tempo di esecuzione, l'uso di code di messaggi associate ai processi". Analogamente, l'istruzione Receive (mess2) si limita ad accettare il messaggio che gli viene passato da buffer (il quale a sua volta si serve di una coda locale, coda_dati, unica per tutti i processi) e quindi non ci sarebbe bisogno che tale istruzione prelevasse un elemento dalla testa di una coda, ma...
Pare dunque che in totale servano tre code: una, gestita dal kernel, cui fa riferimento la RECEIVE del processo buffer, e le due locali del processo buffer. Negli appunti di De Carlini in mio possesso viene sostenuto invece che le code necessarie in totale siano due: 'una all'interno del kernel per gestire le send asincrone dei produttori e una all'interno del processo buffer per gestire i messaggi quando non ci sono consumatori pronti'. Questa affermazione non sembra plausibile.
Si noti che, proprio a motivo della presenza di un solo produttore, non è possibile che il ramo then dell'istruzione if < coda_piena >... sia percorso due volte di seguito, ossia che si esegua la prod_pronto := true una seconda volta senza che la medesima variabile sia stata prima posta a false. Similmente dicasi per cons_pronto.
Privacy |
Articolo informazione
Commentare questo articolo:Non sei registratoDevi essere registrato per commentare ISCRIVITI |
Copiare il codice nella pagina web del tuo sito. |
Copyright InfTub.com 2025