Articolo pubblicato sul n. 109 di MCmicrocomputer (Edizioni Technimedia Srl - Roma) nel luglio 1991

MCmicrocomputer


Multitasking:
OCCAM: parallelismo puro

di Andrea de Prisco

Puo' anche darsi che non abbiate mai sentito la parola OCCAM. Non potete pero' dire altrettanto della parola "transputer". OCCAM, per tagliare corto, e' il linguaggio di programmazione dei transputer. E se quest'ultimo e' il processore piu' "multitasking" che c'e', OCCAM e' il linguaggio di programmazione piu' "parallelo" che c'e'. Tanto parallelo che un programma scritto in OCCAM puo' essere formato da istruzioni dCopertina del numero di MCmicrocomputer contenente l'articoloa eseguire non "una dopo l'altra" ma addirittura contemporaneamente. Si', un intero programma eseguito, dal punto di vista logico, in un solo passo: anche se le istruzioni sono centinaia o migliaia. O, addirittura, formato da pochissime istruzioni che pero' sottindendono un parallelismo a tempo di esecuzione pronto ad esplodere al fatidico "run".

Parallelismo "esplosivo"

Non e' proprio l'argomento di questa puntata, ma tanto per farvi venire ancora di piu' la voglia di continuare a leggere (o, soprattutto, per non farvela passare...) anticipiamo questo mese qualche informazione in piu' riguardo l'ultima frase dell'introduzione di quest'articolo.
Chiunque programma i computer sa che i loop quali FOR-NEXT, WHILE-DO, REPEAT-UNITL servono essenzialmente per non riscrivere tante volte la stessa porzione di codice. Se dobbiamo eseguire 5 volte una sezione di un programma, nessuno si sognerebbe mai di riscriverla 5 volte di seguito (a meno di non avere problemi di velocita') ma semplicemente la incorpora in un loop impostando questo all'esecuzione sequenziale per il numero di volte voluto.
OCCAM, da bravo linguaggio di programmazione parallela, permette di fare la stessa cosa nel "parallelo". Esistono costrutti OCCAM che permettono di lanciare in parallelo tanti processi quanti ne abbiamo impostato nel costrutto stesso: ognuno lavorera' su un certo insieme proprio di dati e il comando di ripetizione parallela terminera' quando saranno terminate tutte le istruzioni lanciato comando stesso e che, ripetiamo, saranno eseguite parallelamente.
Chiariamo con un esempio. Immaginate la sezione di programma Pascal-Like:

for i:=1 to 20
a[i] := 0;

che non fa altro che annullare il vettore 'a'. Qualsiasi linguaggio di programmazione sequenziale azzerera' i vari elementi l'uno dopo l'altro, partendo dal numero 1 fino al numero 20. In OCCAM, volendo, possiamo parallelizzare l'esecuzione scrivendo cosi':

PAR i=1 FOR 20
a[i] := 0

che annullera' simultaneamente i venti elementi del vettore 'a'. Se non ci credete, in parte non avete tutti i torti.
OCCAM non e' stato ancora impementato nella sua pienezza e un'istruzione come quella appena vista, in esecuzione su un singolo transputer sara' eseguita si' in parallelo, ma in parallelismo simulato. In pratica il transputer e' in grado di eseguire piu' processi contemporaneamente ma, ovviamente, grazie al proprio multitasking di macchina. Quindi su un singolo transputer i venti assegnamenti saranno eseguiti l'uno dopo l'altro (anche se non necessariamente in ordine), mentre su una rete di chip di questo tipo sara' possibile l'esecuzione in parallelismo reale.
Come pero' vedremo meglio nei prossimi appuntamenti, il meccanismo della ripetizione parallela e' tutt'altro che un'inutile esercitazione tecnologica in quanto ci permettera', anche sul singolo chip, di implementare in maniera parallela algoritmi intrinsecamente paralleli senza la necessita' di forzare un'implementazione sequenziale degli stessi...
Ne riparleremo piu' approfonditamente a partire dal prossimo mese.

Un programma OCCAM

La struttura di un programma OCCAM e' la seguente:

dichiarazione:
processo

La dichiarazione e' a sua volta... una dichiarazione oppure una dichiarazione seguita da un'altra dichiarazione. Con le dichiarazioni, come in altri linguaggi convenzionali, indichiamo nomi e tipi delle variabili, costanti, array o canali (vedi dopo) che utilizzeremo nel processo che segue. Lo scope dei nomi dichiarati e' sempre locale al processo che segue. Un processo puo' essere un processo primitivo oppure un costrutto (che combina piu' processi primitivi). Questi sono di solo cinque tipi: assegnamenti, processi d'ingresso, processi d'uscita, processi nulli (SKIP) e processi di terminazione (STOP).
Gli assegnamenti (anche qui nulla di nuovo) permettono di associare valori alle variabili precedentemente dichiarate. Come in Pascal il simbolo di assegnamento e' ":=" (duepunti uguale). I processi di ingresso e di uscita permettono la comunicazione. Come nel modello CSP visto lo scorso mese, il comando di uscita e' rappresentato da un punto esclamativo quello di ingresso dal punto interrogativo. Alla sinistra di questi metteremo il nome del canale sul quale effettuare la comunicazione, alla destra l'oggetto da trasferire se si tratta di un comando d'uscita, una variabile targa per il comando d'ingresso. Ad esempio:

Pippo ! 118

invia sul canale "Pippo" l'intero 118, mentre:

Pippo ? A

riceve nella variabile A il valore depositato nel canale dal mittente. In OCCAM tutte le comunicazioni sono simmetriche e sincrone. Ogni canale e' utilizzato da un solo mittente e un solo destinatario e la comunicazione avviene tanto per il primo quanto per il secondo nel medesimo istante logico. Se il mittente arriva all'appuntamento prima del destinatario aspettera' che questo legga il messaggio inviato prima di proseguire per la sua strada. Discorso analogo per destinatario che aspettera' il mittente nel caso in cui dovesse eseguire la sua "receive" prima della corrispondente "send". Come per le variabili il canale, nome e tipo, deve essere dichiarato prima dell'utilizzo da parte del processo. Il tipo del canale corrisponde al tipo del messaggio che dovra' trasportare. Occorre sottolineare che il canale, nella stesura di un programma OCCAM, e' un oggetto logico che diventera' fisico solo nel momento in cui verra' lanciata l'applicazione. Se l'applicazione multitask che stiamo lanciando girera' su un singolo transputer il canale puo' essere inteso come una struttura implementata in memoria in cui il processo mittente inserisce il messaggio da trasferire, il processo destinatario preleva il messaggio in arrivo. Se la stessa applicazione (senza bisogno di ricompilazione alcuna) viene invece lanciata su piu' transputer, processi in esecuzione su chip diversi accedendo al canale di comunicazione, di fatto utilizzeranno i link fisici di cui il transputer dispone e con i quali dialoga con gli altri transputer.
I principali costrutti (per combinare piu' processi primitivi) sono:

SEQ
IF
CASE
WHILE
PAR
ALT

Il primo, il piu' semplice, permette di eseguire piu' processi in sequenza. Ad esempio:

SEQ
a:=1
b:=2
Canale1 ! a+b
Canale2 ? R

i quattro processi indicati dopo SEQ (attenzione che in OCCAM quelle che sembrano normali statement sono processi essi stessi!) saranno eseguiti in sequenza, come in un normale linguaggio di programmazione. In pratica SEQ identifica un blocco ed e' esso stesso (a sua volta) un processo. Quindi possiamo associare a questo alcune dichiarazioni locali che non saranno piu' valide una volta eseguita la sequenza.
Dove termina il costrutto SEQ ? Ovvero: qual e' l'ultima istruzione del SEQ in cui ci troviamo ? In OCCAM i blocchi sono tutti delimitati dall'indentazione: due caratteri a destra per iniziare una sequenza, due caratteri a sinistra per terminarla. Cosi', il frammento di codice:

SEQ
a:=1
b:=2
Canale1 ! a+b
Canale2 ? R
c:=0
d:=a+R

e' in pratica formato da tre processi. Un primo processo SEQ (a sua volta composto da quattro processi) e due processi di assegnamento. Chiaramente il tutto deve poi essere combinato per formare (in qualsiasi altro modo) un ulteriore unico processo combinato dai tre processi appena descritti. Se ad esempio questi tre processi devono essere eseguiti in sequenza bastera' aggiungere un altro SEQ all'inizio scrivendo cosi':

SEQ
SEQ
a:=1
b:=2
Canale1 ! a+b
Canale2 ? R
c:=0
d:=a+R

proviamo allora a completare questo codice per farlo diventare un programma OCCAM. Mancano innanzitutto le dichiarazioni, immaginiamo di voler limitare lo scope della variabile "b" al solo SEQ interno; tutte le altre dichiarazioni sono, per cosi' dire, globali:

INT a, c, d, R:
CHAN OF INT Canale1, Canale2:
SEQ
INT b:
SEQ
a:=1
b:=2
Canale1 ! a+b
Canale2 ? R
c:=0
d:=a+R

La lettura del codice dovrebbe essere abbastanza semplice cosi' come riconoscere in questo la struttura base fornita all'inizio. La dichiarazioni iniziale e' formata da due dichiarazioni, una per le variabili di tipo INT l'altra per i canali anch'essi di tipo INT. Segue l'unico processo SEQ a sua volta formato da altri processi: la dichiarazione della variabile "b" e' locale al SEQ piu' interno quindi non esiste piu' una volta terminata l'esecuzione di questo.
Detto questo non ci rimane che svelarvi il funzionamento degli altri costrutti. Rapidamente:

IF
{Espressione Booleana1}
Processo1
{Espressione Booleana2}
Processo2
:
:
{Espressione BooleanaN}
ProcessoN

e' il costrutto condizionale. In pratica viene eseguito il primo processo (che puo' essere a sua volta primitivo o combinato come ci pare) la cui espressione booleana ha valore TRUE. Da segnalare che se nessuna espressione booleana e' verificata l'intero programma si blocca terminando li' l'esecuzione (comportamento simile al processo primitivo STOP). Per questo e' conveniente prendere gli opportuni provvedimenti prevedendo una uscita finale con espressione booleana costante TRUE magari con processo SKIP se non dobbiamo eseguire nulla in questo caso. Esempio:

IF
a>0
a:= a-1
b>0
b:=b+1
c>0
SEQ
a:=0
b:=0
c:=c-1
TRUE
SKIP


alla terza scelta e' stato associato un processo combinato (un SEQ) quindi nel caso in cui le prime due espressioni danno esito falso e "c" e' maggiore di zero saranno eseguite le tre line indicate. Come caso finale abbiamo inserito un TRUE...SKIP che ci permette di continuare l'esecuzione nel caso in cui nessuna delle tre espressioni logiche dia esito TRUE.
Il costrutto CASE e' molto simile (come al solito) all'IF: e' eseguito il primo processo corrispondente all'espressione che ha lo stesso valore della variabile del CASE. Come nell'IF e' necessario che almeno una alternativa sia verificata pena il blocco del programma. Per forzare una uscita d'emergenza si usa lo statament ELSE. Esempio:

CASE x
1
x:=0
2
x:=5
ELSE
x:=x-1

se x e' uguale ad 1 sara' eseguito x:=0, se x e' uguale a 2 sara' eseguito x:=5, in tutti gli altri casi x sara' diminuito di 1.
Il costrutto WHILE permette di effettuare loop su condizione. Ad esempio:

WHILE a>0
SEQ
x:=x+2
a:=a-x

esegue il processo SEQ (formato a sua volta da due processi di assegnamento) fintantoche' "a" e' maggiore di zero.
Il costrutto PAR (gia' precedentemente citato) permette di eseguire tutti i processi indicati in parallelo. Naturalmente devono esistere le condizioni affinche' l'esecuzione in parallelo sia possibile. E' sintatticamente simile al costrutto SEQ. Ad esempio:

PAR
a:=a+1
Canale ! b
c:=2

esegue in parallelo i tre processi indicati. Si sarebbe avuto errore a tempo di compilazione se, ad esempio, avessimo scritto:

PAR
a:=a+1
Canale ! a
c:=2

(notare la 'a' al posto della 'b' nel processo di uscita) In questo caso non e' possibile l'esecuzione in parallelo dal momento che la variabile 'a' e' contemporaneamente modificata nel primo processo ed utilizzata nel secondo.
Per finire, il costrutto ALT permette di effettuare il controllo del non determinismo come gia' visto negli articoli che hanno preceduto questa puntata.
La forma piu' semplice del costrutto ALT non prevede, per la verita', alcun controllo ma permette di attendere simultaneamente messaggi da piu' canali. Non appena arriva un messaggio su uno dei canali indicati, sara' letto il messaggio ed, eventualmente, eseguito un apposito processo. Esempio:

ALT
chan1 ? x
{processo 1}
chan2 ? y
{processo 2}
chan3 ? z
{processo 3}

La versione "completa" del costrutto ALT permette di aggiungere una espressione logica ad ogni processo di ingresso in modo da ottenere una guardia d'ingresso. Ad esempio:

ALT
(ax>0) & chan1 ? x
{processo 1}
(ay>0) & chan2 ? y
{processo 2}
(az>0) & chan3 ? z
{processo 3}

in questo modo e' possibile restringere il range di canali da testare in funzione del valore delle espressioni booleane che abbiamo inserito prima di ogni processo di ingresso: saranno testati solo i canali le cui espressioni booleane hanno dato esito positivo. Se tutte le espressioni booleane danno esito negativo il programma si blocca: anche in questo caso e' necessario prevedere una via d'uscita tramite la solita costante TRUE.

REPLICATOR

Non si tratta di un film di fantascienza anche se, per alcuni versi potremmo definirla tale. Ai costrutti SEQ, PAR, ALT e IF e' possibile aggiungere un indice in modo da far "ciclare" il costrutto stesso. In questa sede analizzaremo solo SEQ e PAR che utilizzeremo piu' spesso in seguito, rimandanto le altre spiegazioni al momento piu' opportuno. Il costrutto:

SEQ variabile = valore FOR volte
{processi}

esegue i processi indicati tante volte quante indicate dopo la parola chiave FOR assegnando alla variabile indicata il valore iniziate di volta in volta incrementato di uno. Ad esempio:

SEQ i=0 FOR 5
x = i*2
canale ! x

equivale a scrivere:


SEQ
x = 0
canale ! x
x = 2
canale ! x
x = 4
canale ! x
x = 6
canale ! x
x = 8
canale ! x


Il costrutto:

PAR variabile = valore FOR volte
{processi}

Esegue i processi indicati in parallelo tante volte quante indicate dopo la parola chiave FOR utilizzando in ogni processo lanciato un diverso indice della variabile indicata.
Come nel normale PAR non deve verificarsi che una qualsiasi variabile modificata da un processo parallelo sia contemporaneamente utilizzata da un altro processo parallelo.

Conclusioni

Non facciamo in tempo, questo mese, a mostrarvi un esempio di programma OCCAM. Tenete duro fino al prossimo MC: intanto, tra un bagno ed una rilassante passeggiata in riva al mare, studiatevi un po' tutto quello che abbiamo mostrato in questa puntata. Vi assicuro comunque che ne vedremo delle belle...


Articolo pubblicato su www.digiTANTO.it - per ulteriori informazioni clicca qui