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

MCmicrocomputer


Multitasking:
Cooperazione ad ambiente locale

di Andrea de Prisco

Siano finalmente giunti alla puntata "centrale" della rubrica Multitasking. Da questo numero in poi, la nostra attenzione sara' focalizzata sui meccanismi di comunicazione a "scambio messaggi", propria della cooperazione ad ambiente locale, ma spesso presente anche in linguaggi di programmazione paralleli piu' propriamente ad ambiente globale.Copertina del numero di MCmicrocomputer contenente l'articolo

La netta distinzione

Nelle puntate precedenti abbiamo mostrato alcuni meccanismi di cooperazione-sincronizzazione basati sull'utilizzo (arbitrato) di strutture dati condivise. La condivisione di dati da parte di piu' processi paralleli e' tipica della cooperazione ad ambiente globale. E' proprio all'interno di questo che sono presenti le strutture comuni, e un certo numero di meccanismi messi a disposizione dal sistema permette di accedervi in maniera mutuamente esclusiva. Abbiamo cosi' parlato di sospensione del multitasking, operazioni di Lock-UnLock, semafori, monitor: tutte con l'unico scopo di scongiurare disastrose collisioni tra processi paralleli.
A partire da questo numero, se volete, potete anche dimenticare tutto quanto narratovi (o quasi). Infatti nella cooperazione ad ambiente locale non esiste alcuna risorsa (o struttura) condivisa, ma tutto e' regolato dai processi stessi. Processi, naturalmente, in grado di comunicare, ma sicuramente meno inclini a fare danni "in giro per il sistema". Cio' significa, tra l'altro, che non esistendo piu' strutture dati e risorse condivise, queste sono sempre e comunque inglobate all'interno di processi. Tanto per fare subito un esempio, se due o piu' processi hanno bisogno di un buffer per effettuare operazioni di inserimento ed estrazione di oggetti, nel caso "ambiente globale" tale buffer sarebbe stata una risorsa condivisa il cui accesso viene regolato dalle solite P e V o da un monitor; nel caso "ambiente locale" e' necessario creare un processo che al suo interno ha come struttura privata il buffer e tramite scambio messaggi con gli altri processi effettua inserimenti ed estrazioni di oggetti (figura 1).

Comunicazioni inter process

In figura 2 e' mostrata schematicamente una coppia di processi in grado di comunicare tra loro. E' il caso piu' semplice: abbiamo un processo A che produce dati da inviare a B che li utilizza. Sempre in figura 2 la freccia che collega i due processi e' il canale di comunicazione tra questi. E non e', come potrebbe sembrare, una struttura condivisa dai due processi in quanto a livello di questi il canale non e' visibile. E' si' un oggetto condiviso, ma ad un livello piu' basso: al livello del nucleo del sistema operativo. Infatti tanto A quanto B non hanno visione del canale ma della comunicazione in una forma piu' astratta. A manda messaggi a B, B riceve messaggi da A: che ci sia di mezzo un canale lo sappiano noi e il sistema operativo, ma non i processi. Questo, naturalmente, quando la cooperazione e' realmente ad ambiente locale: in realta' il piu' delle volte i linguaggi di programmazione parallela non sono cosi' "puliti" fin in fondo e spesso lasciano visione all'interno dei processi anche di oggetti non meglio identificati ma comunque, almeno per certi versi, condivisi. Ma non scendiamo troppo nei dettagli subito: avremo modo di approfondire meglio l'argomento in seguito.
Dicevamo che il canale e' un oggetto che permette la comunicazione tra processi ma non e' direttamente visibile da questi. Un po' come il Program Counter nella programmazione in assembler (quando questo non e' allocato in un normale registro di macchina, ndr) o i vari puntatori alle aree dati nei linguaggi di programmazione ad alto livello.
Il canale e' dunque direttamente (nonche' esclusivamente) utilizzato dal nucleo del sistema operativo per effettuare la comunicazione.
Cio' che avviene all'interno dei processi (come in figura 2) e' che il processo mittente ad un certo punto esegue una operazione di "send" per spedire un messaggio (ad esempio un dato) al processo B. Questo, per ricevere l'informazione in arrivo da A, analogamente eseguira' la sua operazione di comunicazione che nel caso del processo destinatario sara' una "receive". La "send" avra' tra i suoi parametri l'oggetto da spedire, la "receive" una variabile dello stesso tipo nella quale ricevera' l'oggetto trasmesso. In pratica a trasmissione avvenuta l'effetto finale sara' quello di assegnare alla variabile indicata nella funzione "receive" del processo destinatario il valore dell'oggetto trasmesso dal mittente e indicato nella funzione "send".
Il canale, conseguentemente, eredita il tipo dell'oggetto trasmesso che come abbiamo detto e' anche uguale a quello della variabile targa del processo destinatario.
Ma per definire un canale non basta indicare il suo tipo ma occorre in qualche modo definire anche mittente e destinatario della comunicazione. Esistono fondamentalmente due strade: canali con nomi espliciti dei processi (figura 3a) e canali con porte (figira 3b). Ricordando che tale definizione non spetta ne' al processo mittende ne' al processo destinatario di una comunicazione ma semplicemente al sistema operativo che deve avere un "quadro chiaro" di tutta la situazione, e' al momento della compilazione dei vari processi che sono definiti implicitamente anche i vari canali.
Nel caso di canali con nomi espliciti dei moduli il canale e' definito dalla tripla ordinata:

(Mittente, Destinatario, Tipo)

e viene dedotta dal sistema in base alle operazioni di "send" presenti nei processi mittenti e alle operazioni di "receive" nei processi destinatari che contengono come primo parametro il nome del processo partner. Se ad esempio nel processo A troviamo:

send(B, messaggio)

nel processo B:

receive(A, variabile)

e tanto "messaggio" quanto "variabile" sono dello stesso tipo T, il canale e' definito dalla tripa:

(A, B, T)

Nel caso di canali con porte il canale e' definito dalla coppia ordinata:

(PortaMittente, PortaDestinatario)

Le porte sono oggetti privati dei processi e rappresentano delle interfacce logiche tra i processi e i canali di comunicazione. Hanno lo stesso tipo del messaggio da trasmettere e quindi del canale che interfacciano.
Nelle operazioni di "send" presenti nei processi mittenti e nelle operazioni di "receive" nei processi destinatari si fa esplicito riferimento non ai processi partner ma alla propria porta di ingresso o di uscita precedentemente definita.
Cio' che e' ora necessario e' "linkare", prima dell'utilizzo, una porta con un processo, sia nel caso del mittente (che linka la sua porta al nome del processo destinatario) che nel caso del destinatario (che linka la sua porta al nome del processo mittente).
Se ad esempio nel processo A troviamo:

out port PA: T
var messaggio: T
bind(PA, B)
send(PA, messaggio)

nel processo B:

in port PB: T
var variabile: T
bind(PB, A)
receive(PB, variabile)

il canale e' definito dalla coppia:

(PA, PB)

e non e' piu' presente il tipo in quanto gia' definito nella dichiarazione delle porte.


Comunicazioni sincrone e asincrone

C'e' un particolare che abbiamo volutamente sorvolato fino ad ora: la sincronia o asincronia delle comunicazioni. Cosa succede, per dirla in breve, se, quando il mittende effettua una "send", il destinatario non effettua la corrispondente "receive" perche' momentaneamente indaffarato in altri lavori? Il mittente deposita il messaggio e continua oppure attende pazientemente il destinatario per consegnare personalmente il messaggio? Esistono in genere tutt'e due le possibilita': e, naturalmente, non esiste la soluzione migliore, ma possono essere utili l'una o l'altra possibilita' a seconda dei casi.
E' chiaro, inoltre, che la comunicazione asincrona e' possibile solo associando al canale un'area di memoria dove immagazzinare i messaggi spediti e non ancora letti.
Discorso del tutto analogo per le operazioni di "receive". Cosa deve fare il destinatario se un messaggio richiesto non e' stato ancora spedito? Resta li' impalato ad aspettare oppure continua per la sua strada senza il messaggio richiesto? Come nel caso precedente puo' essere utile disporre di entrambe le soluzioni a seconda dei casi. Nei prossimi numeri, quando cominceremo a mostrare alcuni programmi multitask vedremo come utilizare a seconda dei casi le varie chance disponibili.

Forme di comunicazione

In figura 4 sono mostrate le forme di comunicazione base. La prima, la piu' semplice, prevede semplicemente che vi sia nella comunicazione un solo mittente e un solo destinatario (comunicazione simmetrica). E' il caso che abbiamo gia' utilizzato negli esempi precedenti in cui un processo invia e un processo riceve attraverso un canale. Naturalmente e' possibile instaurare una nuova comunicazione anche dal processo destinatario verso il mittente (invertendo i ruoli) attraverso un nuovo canale diverso dal primo. Infatti, posto che il tipo dei dati che i due processo si scambiano vicendevolmente e' lo stesso, i due canali sono diversi essendo differenti il mittente e il destinatario nei due casi. Il primo canale sara' ad esempio identificato dalla terna:

(A, B, T)

il secondo dalla terna:

(B, A, T)

Le forme di comunicazione asimmetriche (rappresentate in figura 4b e 4c) consentono rispettivamente l'esistenza di piu' mittenti o piu' destinatari sul medesimo canale. La comunicazione asimmetrica in uscita (figura 4c) e' detta anche comunicazione per diffusione in quanto il medesimo messaggio e' inviato simultaneamente a piu' destinatari.
Nel primo caso della comunicazione asimmetrica in ingresso, si tratta di messaggi diversi (provenienti da processi diversi) che si accodano sul canale e che verranno letti in istanti successivi dal processo destinatario. Tanto i mittenti di figura 4b quanto di destinatari di figura 4c non sono a conoscenza dell'asimmetria del canale: per loro la comunicazione e' sempre e comunque simmetrica (il loro partner nella comunicazione e' unico). Vice versa il destinatario di una comunicazione asimmetrica in ingresso e il mittente di una comunicazione asimmetrica in uscita sono coscienti della asimmetria in quanto il primo dovra' esplicitamente indicare da quali processi (piu' d'uno) ricevere il secondo a quali processi spedire.

Il nondeterminismo

Se fino ad oggi pensavate che gli attuali calcolatori fossero delle macchine deterministiche avete una visione rigidamente sequenziale dei calcolatori. Oggi, fermo restando la determinicita' di un singolo processo (che ad ogni sequenza di input fornisce sempre un'unica sequenza di output), le cose stanno ben diversamente nelle macchine multitasking. Infatti a livello di processi nessuna ipotesi e' fatta sull'avanzamento parallelo di questi, ne' sull'indeterminatezza di alcune situazioni che possono verificarsi. L'esempio tipico e' mostrato proprio in figura 4b: i processi A1, A2, A3 sono assolutamente indipendenti; utilizzano (senza saperlo) lo stesso canale verso B il quale riceve sequenzialmente i messaggi dai tre processi mittenti. Se nello stesso istante logico piu' processi inviano messaggi a B non e' definito a priori in quale sequenza questi saranno recapitati al destinatario. Ne' B potra' fare nulla per forzare una sua preferenza o priorita'.
E se in alcuni casi possiamo procedere incuranti del problema, in altri potrebbe essere necessario pilotare in qualche modo la scelta del messaggio da ricevere per primo.
Naturalmente in questo caso non avremo piu' un unico canale asimmetrico ma tanti canali simmetrici quanti sono i mittenti (figura 5). All'interno del destinatario la receive avra' come parametri tante triple quanti sono i mittenti. Ogni tripla sara' formata da una variabile targa (nella quale, eventualmente, ricevere il messaggio), il nome del mittente e una variabile di condizionamento (ad esempio un'espressione logica) che maschera o meno la lettura da quel canale. Cosi' se abbiamo 5 canali e altrettanti processi che spediscono e vogliamo ricevere solo dai primi due processi sara' sufficiente far valere TRUE i primi due parametri booleani e FALSE i rimanenti tre.

Niente paura

Certo, per chi ha utilizzato per proprie risorse di calcolo sempre in maniera sequenziale questo linguaggio potra' sembrare anche un tantino ostico, ma per fortuna una volta presa la mano tutto torna ad essere semplice e magicamente affascinante.
Tra gli utenti dei piccoli sistemi i piu' fortunati sono gli utenti Amiga che dispongono di un vero e proprio computer multitask con tanto di processi, messaggi, porte, risorse, anche se non sempre gestiti o gestibili nel piu' pulito dei modi.
Gli utenti MS-DOS per giocare "al multitasking" hanno invece una possibilita' diversa, molto piu' interessante, anche se poco proponibile dal punto di vista economico: acquistare una scheda per il loro PC dotata di almeno un transputer e due mega di ram e utilizzare su questa il linguaggio OCCAM.
Cosi' come faremo noi a partire dal prossimo numero: metteremo un po' il naso nel multitask che ci circonda per spulciare insieme le cose piu' interessanti dal punto di vista didattico e applicativo. Inutile ricordarvi che sono, come sempre, ben accette tutte le collaborazioni dei lettori che perverranno in redazione. Per il momento buon lavoro e appuntamento al prossimo mese.


Impaginato originale...


Clicca per ingrandire... Clicca per ingrandire...
Clicca per ingrandire...  

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