Nota: la simultaneità viene qui affrontata in maniera generica, come potrebbe essere applicata
a qualsiasi sistema. Tuttavia, la simultaneità è particolarmente importante nei sistemi che devono reagire
a degli eventi esterni in tempo reale e che spesso hanno delle difficili scadenze da rispettare. Per
affrontare la particolare domanda di questa classe di sistema, RUP (Rational Unified Process) dispone di
estensioni di sistema real-time (reattivi). Per ulteriori informazioni su questo argomento, consultare Sistemi real-time.
|
La simultaneità è la tendenza delle cose ad accadere nello stesso momento nel sistema. Naturalmente la simultaneità è
un fenomeno normale. Nella realtà, in qualsiasi momento, molte cose accadono simultaneamente. Quando si progetta un
software per monitorare e controllare i sistemi del mondo reale, si ha a che fare con questa simultaneità naturale.
Quando si affrontano problematiche di simultaneità nei sistemi software, esistono generalmente due aspetti importanti:
essere in grado di rilevare e di rispondere a degli eventi esterni che si verificano in ordine casuale e assicurarsi
che a quegli eventi vengano date delle risposte in un intervallo minimo richiesto.
Se ogni attività simultanea si fosse evoluta in modo indipendente, in maniera davvero parallela, la questione sarebbe
relativamente facile: basterebbe semplicemente creare dei programmi separati che si occupano di ciascuna attività. Le
sfide della progettazione di sistemi simultanei nascono soprattutto a causa delle interazioni che si verificano fra le
attività simultanee. Quando le attività simultanee interagiscono, è richiesta una coordinazione.
Figura 1: Esempio di simultaneità all'opera: attività parallele che non interagiscono hanno delle problematiche
di simultaneità semplici. E' quando le attività parallele interagiscono con le stesse risorse o le condividono che le
problematiche della simultaneità divengono importanti.
Il traffico dei veicoli fornisce un'utile analogia. I flussi di traffico paralleli su strade diverse con scarsa
interazione causano pochi problemi. I flussi paralleli su corsie adiacenti richiedono coordinazione, per avere
un'interazione sicura, ma negli incroci si verifica un'interazione di tipo molto più serio, che richiede una
coordinazione più attenta.
Alcune delle forze che dirigono la simultaneità sono esterne. Vale a dire che sono imposte dalle esigenze
dell'ambiente. Nei sistemi del mondo reale molte cose accadono simultaneamente e devono essere risolte "in tempo reale"
dal software. Per fare ciò molti sistemi real-time devono essere "reattivi." Devono rispondere agli eventi generati
esternamente che possono verificarsi in momenti a caso, in ordine casuale o entrambi.
La progettazione di un programma procedurale convenzionale che si occupi di queste situazioni è estremamente complessa.
Può essere molto più semplice suddividere il sistema in elementi software simultanei che si occupino di ognuno di
questi eventi. La frase chiave è "può essere", poiché anche la complessità viene toccata dal grado di interazione fra
gli eventi.
Possono anche esserci delle motivazioni ispirate internamente, per la simultaneità [LEA97].
L'esecuzione di attività in parallelo può velocizzare in maniera sostanziale il lavoro di calcolo di un sistema, se
sono disponibili più CPU. Persino all'interno del singolo processore, il multitasking può velocizzare sensibilmente le
cose impedendo ad un'attività di bloccarne un'altra in attesa di I/O, ad esempio. Una situazione comune in cui questo
si verifica è durante l'avvio del sistema. Spesso sono presenti molti componenti, ognuno dei quali richiede del tempo
prima di essere operativo. L'esecuzione di queste operazioni in modo sequenziale può essere estremamente lenta.
Con la simultaneità può essere migliorata anche la controllabilità del sistema. Ad esempio, una funzione può essere
avviata, arrestata o altrimenti influenzata nel corso del flusso da altre funzioni simultanee (una cosa estremamente
difficile da ottenere senza componenti simultanei).
Con tutti i questi vantaggi, perché la programmazione simultanea non viene utilizzata ovunque?
La maggior parte dei computer e dei linguaggi di programmazione sono intrinsecamente sequenziali. Una procedura o un
processore esegue un'istruzione alla volta. In un singolo processore sequenziale, l'illusione di simultaneità deve
essere creata inserendo l'esecuzione di attività diverse. Le difficoltà giacciono non tanto nelle meccaniche di
effettuazione ma nella determinazione di quando e come inserire i segmenti del programma che possono interagire fra di
loro.
Sebbene l'ottenimento della simultaneità sia facile con i processori multipli, le interazioni divengono più complesse.
Innanzitutto vi è la questione delle comunicazioni fra attività in esecuzione su processori diversi. In genere sono
implicati diversi livelli di software, che aumenta la complessità ed aggiunge delle spese di sincronizzazione. Il
determinismo viene ridotto nei sistemi con più CPU, poiché gli orologi e la sincronizzazione possono differire, ed i
componenti possono fallire in modo indipendente.
Infine, i sistemi simultanei possono essere più difficili da capire perché mancano di uno stato esplicito di sistema
globale. Lo stato di un sistema simultaneo è l'aggregazione degli stati dei suoi componenti.
Come esempio per illustrare i concetti da trattare, viene utilizzato un sistema di ascensori. Più precisamente, un
sistema di computer progettato per controllare un gruppo di ascensori in un unico luogo, dentro un edificio. Ovviamente
possono esserci molte attività simultaneamente in un gruppo di ascensori, oppure nessuna! In qualsiasi momento qualcuno
ad un piano qualsiasi può chiamare l'ascensore, e delle altre richieste potrebbero essere in attesa. Alcuni degli
ascensori potrebbero essere fermi mentre altri potrebbero avere dei passeggeri o essere in procinto di rispondere ad
una chiamata o entrambe le cose. Le porte si devono aprire e chiudere al momento giusto. I passeggeri potrebbero
ostruire le porte oppure premere i pulsanti di apertura o chiusura della porta, oppure selezionare i piani e poi
cambiare idea. I display devono essere aggiornati, i motori controllati e così via, tutto sotto la supervisione del
sistema di controllo degli ascensori. Nel complesso è un buon modello di esplorazione dei concetti di simultaneità, ed
uno per il quale si condivide un ragionevole grado comune di comprensione ed un vocabolario funzionante.

Figura 2: Uno scenario che implica due ascensori e cinque potenziali passeggeri distribuiti su 11 piani.
Poiché i potenziali passeggeri effettuano la chiamata al sistema in momenti diversi, il sistema tenta di fornire il
miglior servizio complessivo selezionando gli ascensori che devono rispondere alla chiamata in base al loro stato
corrente e ai tempi di risposta proiettati. Ad esempio, quando il primo potenziale passeggero, Andy, chiama l'ascensore
per scendere ai piani inferiori, entrambi gli ascensori sono fermi, quindi il più vicino, Ascensore 2, risponde, anche
se deve prima salire per prendere Andy. D'altra parte, pochi istanti dopo, quando il secondo potenziale passeggero,
Bob, chiama l'ascensore per salire, risponde l'ascensore 1, il più distante, perché è noto che l'ascensore 2 deve
andare ai piani inferiori in una destinazione ancora non nota, prima di poter rispondere da giù ad una chiamata verso i
piani superiori.
Se il sistema di ascensori avesse avuto un solo ascensore che poteva servire solo un passeggero alla volta, si sarebbe
tentati di pensare che sarebbe stato gestibile con un normale programma sequenziale. Persino per questo "semplice" caso
il programma richiederebbe molte diramazioni per sistemare le diverse condizioni. Ad esempio, se il passeggero non
fosse mai salito sull'ascensore ed avesse selezionato un piano, sarebbe utile riazzerare l'ascensore per consentirgli
di rispondere ad un'altra chiamata.
Il normale requisito per gestire le chiamate provenienti da più potenziali passeggeri e le richieste di più passeggeri
esemplifica le forse esterne, di cui si parlava prima, che dirigono la simultaneità. Poiché i potenziali passeggeri
conducono le loro vite simultanee, effettuano le chiamate all'ascensore in momenti apparentemente a caso, a prescindere
dallo stato dell'ascensore. E' estremamente difficile progettare un programma sequenziale che possa rispondere a uno
qualsiasi di questi eventi esterni in qualunque momento, pur continuando a guidare l'ascensore secondo le decisioni
passate.
Per poter progettare sistemi simultanei in maniera efficace, è necessario ragionare sul ruolo della simultaneità nel
sistema e per far questo sono necessarie le astrazioni della simultaneità stessa.
I blocchi fondamentali per la creazione di sistemi simultanei sono le "attività" che procedono più o meno in maniera
indipendente le une dalle altre. Un'utile astrazione grafica per riflettere su queste attività è il "timethread" di
Buhr (thread temporale). [BUH96] Lo
scenario degli ascensori della Figura 3 in verità ne ha utilizzato un formato. Ogni attività è rappresentata da una
linea lungo la quale viaggia l'attività. I grossi punti rappresentano la posizione in cui un attività inizia o attende
che si verifichi un evento, prima di continuare. Un'attività può stimolare l'altra a continuare, che viene
rappresentata nella notazione del timethread toccando il punto di attesa sull'altro timethread.
Figura 3: Una visualizzazione dei thread di esecuzione
I blocchi di base del software sono delle procedure e delle strutture di dati ma queste da sole sono inadeguate per le
riflessioni sulla simultaneità. Quando il processore esegue una procedura, segue un particolare percorso a seconda
delle condizioni correnti. Questo percorso può essere denominato "thread di esecuzione" o "thread di controllo". Il
thread di controllo può prendere diverse diramazioni o loop, a seconda delle condizioni esistenti al momento, e nei
sistemi real-time si può sospendere per un periodo specificato o attendere un orario pianificato per riprendere.
Da un punto di vista del progettista di programmi, il thread di esecuzione viene controllato dalla logica del programma
e viene pianificato dal sistema operativo. Quando il progettista di software sceglie di avere una procedura che
richiama le altre, il thread di esecuzione salta da una procedura all'altra, per poi tornare indietro a continuare da
dove aveva interrotto, quando viene rilevata un'istruzione di ritorno.
Da un punto di vista della CPU, esiste solo un thread principale di esecuzione che si inserisce nel software, integrato
da brevi thread separati che vengono eseguiti in risposta alle interruzioni hardware. Poiché tutto si costruisce su
questo modello, è importante per i progettisti conoscerlo. I progettisti di sistema real-time, molto più che dei
progettisti di altri tipi di software, devono comprendere il funzionamento di un sistema ad un livello molto
dettagliato. Questo modello, tuttavia, è ad un livello talmente basso di astrazione che può rappresentare solo una
granularità molto scadente della simultaneità (quella della CPU). Per progettare dei sistemi complessi, è utile poter
lavorare su vari livelli di astrazione. L'astrazione, naturalmente, è la creazione di una vista o di un modello che
omette i dettagli non necessari, in modo da poter concentrare l'attenzione su ciò che è importante per il problema
presente.
Per spostarsi di un livello, in genere si pensa al software in termini di strati. Al livello base, il sistema operativo
(OS) si colloca fra l'hardware ed il software applicativo. Fornisce all'applicazione i servizi basati sull'hardware, ad
esempio la memoria, la tempificazione e l'I/O, ma rende astratta la CPU per creare una macchina virtuale indipendente
dalla configurazione hardware effettiva.
Per supportare la simultaneità un sistema deve fornire più thread di controllo. L'astrazione di un thread di controllo
può essere implementata in diversi modi dell'hardware e dal software. I meccanismi più comuni sono le variazioni di uno
dei seguenti elementi [DEI84], [TAN86]:
-
Multiprocessing - più CPU in esecuzione simultaneamente
-
Multitasking - i sistemi operativi simulano la simultaneità su una singola CPU
inserendo l'esecuzione di diverse attività
-
Soluzioni basate su applicazioni - il software applicativo si assume la responsabilità
di passare nelle differenti diramazioni del codice nei tempi appropriati
Quando il sistema operativo fornisce il multitasking, una unità comune della simultaneità è il processo. Un processo è
un'entità fornita, supportata e gestita dal sistema operativo il cui unico scopo è di fornire un ambiente in cui
eseguire un programma. Il processo fornisce uno spazio memoria ad uso esclusivo del suo programma applicativo, un
thread di esecuzione per eseguirlo e forse dei mezzi per inviare messaggi ad altri processi e riceverne. In effetti il
processo è una CPU virtuale per l'esecuzione di una parte simultanea di un'applicazione.
Ogni processo ha tre possibili stati:
-
bloccato - in attesa di ricevere dell'input o di ottenere il controllo di qualche risorsa;
-
pronto - in attesa che il sistema operativo gli dia un comando da eseguire;
-
in esecuzione - al momento sta utilizzando la CPU.
Ai processi spesso vengono anche assegnate le relative priorità. Il kernel del sistema operativo determina quale
processo eseguire in qualsiasi momento specificato, in base al loro stato, le loro priorità e dei criteri di
pianificazione. I sistemi operativi multitasking in realtà condividono un singolo thread di controllo fra tutti i loro
processi.
Nota: I termini 'attività' e 'processo' vengono spesso utilizzati indifferentemente. Sfortunatamente il termine
'multitasking' viene utilizzato in genere per indicare la possibilità di gestire più processi alla volta, mentre
'multiprocessing' si riferisce ad un sistema con più processori (CPU). Si aderisce a questa convenzione perché è la più
comunemente accettata. Tuttavia di tanto in tanto viene utilizzato il termine 'attività', per fare una sottile
distinzione fra unità di lavoro eseguita (l'attività) e l'entità che fornisce le risorse e l'ambiente per il processo.
Come si è già detto in precedenza, dal punto di vista della CPU, esiste solo un thread di esecuzione. Proprio come un
programma applicativo può saltare da una procedura ad un'altra richiamando delle routine secondarie, il sistema
operativo può trasferire il controllo da un processo ad un altro in caso di interruzione, del completamento di una
procedura o qualche altro evento. A causa della protezione della memoria consentita da un processo, questo "passaggio
da un'attività all'altra" può portare a delle spese considerevoli. Inoltre, poiché i criteri di pianificazione e gli
stati del processo hanno poco a che vedere con il punto di vista dell'applicazione, l'inserimento di processi in genere
è di un livello troppo basso di astrazione per poter pensare al tipo di simultaneità che è importante per
l'applicazione.
Per poter ragionare con chiarezza sulla simultaneità, è importante mantenere una netta separazione fra il concetto di
thread di esecuzione e quello di scambio di passaggio attività. Ogni processo può essere considerato come gestore del
proprio thread di esecuzione. Quando il sistema operativo passa da un processo all'altro, un thread di esecuzione viene
temporaneamente interrotto ed in altro si avvia o riprende l'attività dal punto in cui l'aveva lasciata.
Molti sistemi operativi, in particolare quelli utilizzati per applicazioni real-time, offrono un'alternativa "più
leggera" ai processi, denominata "thread" o "thread leggero."
I thread sono un modo per ottenere una granularità leggermente più sottile di simultaneità all'interno di un processo.
Ogni thread appartiene ad un singolo processo e tutti i thread di un processo condividono lo spazio di memoria singolo
ed altre risorse controllate da quel processo.
Di solito ad ogni thread viene assegnata una procedura da eseguire.
Nota: sfortunatamente il termine 'thread' a più significati. Quando si utilizza la parola 'thread' da sola, come
in questo caso, si riferisce ad un 'thread fisico' fornito e gestito dal sistema operativo. Quando si parla di 'thread
di esecuzione' o di 'thread di controllo' o di 'timethread', come nelle discussioni che seguiranno, si intende
un'astrazione che non è necessariamente associata ad un thread fisico.
Naturalmente più processori offrono l'opportunità di una vera esecuzione simultanea. Più comunemente, ogni attività
viene assegnata in modo permanente ad un processo di un particolare processore ma in determinate circostanze le
attività possono essere assegnate dinamicamente al successivo processore disponibile. Forse il metodo più accessibile
per ottenere ciò è l'utilizzo di un "multiprocessore simmetrico". In una configurazione hardware di questo tipo più CPU
possono accedere alla memoria attraverso un bus comune.
I sistemi operativi che supportano i multiprocessori simmetrici possono assegnare dinamicamente i thread a qualunque
CPU disponibile. Degli esempi di sistemi operativi che supportano i multiprocessori simmetrici sono SUN Solaris e
Microsoft Windows NT.
In precedenza abbiamo fatto delle asserzioni apparentemente paradossali, cioè che la simultaneità aumenta e diminuisce
la complessità del software. Il software simultaneo fornisce delle soluzioni più semplici ai problemi complessi
principalmente perché consente una "separazione delle problematiche" fra le attività simultanee. Sotto questo aspetto
la simultaneità è solo un tool in più con il quale aumentare la modularità del software. Quando un sistema deve
eseguire prevalentemente delle attività indipendenti o deve rispondere ad eventi prevalentemente indipendenti, la loro
assegnazione a componenti simultanei individuali naturalmente semplifica la progettazione.
Le ulteriori complessità associate al software simultaneo sorgono quasi sempre da situazioni in cui le attività
simultanee sono quasi indipendenti ma non del tutto. In altre parole, le complessità nascono dalle relative
interazioni. Da un punto di vista pratico, le interazioni fra le attività asincrone implicano invariabilmente lo
scambio di qualche genere di segnale o di informazioni. Le interazioni fra i thread di controllo simultanei fanno
nascere una serie di problematiche univoche per i sistemi simultanei e che devono essere risolte per garantire un
corretto funzionamento del sistema.
Anche se esistono molte diverse realizzazioni specifiche di IPC (comunicazione fra processi) o di meccanismi di
comunicazione fra thread, alla fine possono essere tutte classificate in due categorie:
Nelle comunicazioni asincrone l'attività di invio inoltra le informazioni a prescindere che il destinatario sia
pronto a riceverle o meno. Dopo aver avviato l'invio delle informazioni, il mittente procede con la sua successiva
attività. Se il destinatario non è pronto a ricevere le informazioni, queste vengono collocate in una coda, dove
successivamente il destinatario potrà andarle a recuperare. Sia il mittente che il destinatario operano in modo
asincrono l'uno rispetto all'altro, e quindi non possono effettuare supposizioni sullo stato dell'altro. La
comunicazione asincrona spesso viene denominata passaggio di messaggi.
La comunicazione sincrona include la sincronizzazione fra il mittente ed il destinatario, oltre allo scambio di
informazioni. Durante lo scambio di informazioni, le due attività simultanee si fondono eseguendo, in effetti, un
segmento di codice condiviso, e poi si dividono di nuovo quando la comunicazione è completata. Quindi, durante
quell'intervallo sono sincronizzate fra loro e immuni da conflitti di simultaneità. Se un'attività (mittente o
destinatario) è pronta per comunicare prima dell'altra, verrà sospesa finché anche l'altra non è pronta. Per questo
motivo questa modalità di comunicazione a volte viene denominata rendezvous.
Un potenziale problema con la comunicazione sincrona è che, quando sul peer è in attesa di essere pronta, un'attività
non è in grado di reagire a nessun altro evento. Per molti sistemi real-time questo non è sempre accettabile perché
potrebbe non essere possibile garantire una pronta risposta ad una situazione importante. Un altro svantaggio è che è
propenso ai deadlock. Un deadlock si verifica quando due o più attività sono coinvolte in un circolo vizioso di
attesa reciproca.
Quando sono necessarie delle interazioni fra le attività simultanee, il progettista deve scegliere fra lo stile
sincrono o asincrono. Per sincrono si intende che due o più thread di controllo simultanei devono effettuare un
rendezvous in un singolo punto nel tempo. In genere questo significa che un thread di controllo deve attenderne un
altro per rispondere ad una richiesta. La forma più semplice e più comune di interazione sincrona si verifica quando
l'attività simultanea A richiede informazioni dall'attività simultanea B per poter procedere con il proprio lavoro.
Le interazioni sincrone sono, naturalmente, la norma per i componenti software non simultanei. Le normali chiamate di
procedura sono il principale esempio di interazione sincrona: quando una procedura ne chiama un'altra, il chiamante
trasferisce immediatamente il controllo alla procedura chiamata ed effettivamente "attende" che il controllo venga
ritrasferito indietro. Nel mondo simultaneo, tuttavia, è necessario dell'apparato aggiuntivo per sincronizzare i thread
di controllo altrimenti indipendenti.
Le interazioni asincrone non richiedono un rendezvous nel tempo, ma richiedono tuttavia dell'apparato aggiuntivo per
supportare la comunicazione fra i due thread di controllo. Spesso questo apparato prende la forma di canali di
comunicazione con code di messaggi, in modo da poter inviare e ricevere messaggi in maniera asincrona.
Una singola applicazione può mischiare la comunicazione sincrona e quella asincrona, a seconda che debba attendere una
risposta o abbia altro lavoro da svolgere mentre il destinatario elabora il messaggio.
La vera simultaneità dei processi o thread è possibile solo sui multiprocessori con esecuzione simultanea dei processi
o thread; sui singoli processori l'illusione di un esecuzione simultanea di thread o processi viene creata dallo
scheduler del sistema operativo, che suddivide le risorse di elaborazione disponibili in piccoli gruppi, così da
sembrare che diversi thread o processi siano in esecuzione contemporaneamente. Una scarsa progettazione sconfiggerebbe
questa suddivisione del tempo creando più processi o thread che comunicano frequentemente e in modo sincrono, facendo
sì che i processi o thread utilizzino la maggior parte della loro "fetta di tempo" efficacemente bloccati e in attesa
di risposta da un altro processo o thread.
Le attività simultanee possono dipendere da scarse risorse che devono essere condivise fra loro. Degli esempi tipici
sono le unità di I/O. Se un'attività richiede un risorsa che è utilizzata da un'altra attività, deve attendere il suo
turno.
Forse la problematica fondamentale della progettazione di sistemi simultanei è di evitare le "condizioni di
competizione". Quando parte del sistema deve eseguire delle funzioni che dipendono dallo stato (cioè funzioni i cui
risultati dipendono dallo stato corrente del sistema), ci si deve accertare che lo stato sia stabile durante
l'operazione. In altre parole, determinate operazioni devono essere "atomiche". Quando due o più thread di controllo
hanno accesso allo stesse informazioni di stato, è necessaria una forma di "controllo di simultaneità" per assicurare
che il thread non modifichi lo stato mentre l'altro sta eseguendo un'operazione atomica dipendente dallo stato.
Tentativi simultanei di accesso alle stesse informazioni, che potrebbero rendere lo stato internamente incoerente,
vengono denominati "condizioni di competizione".
Un tipico esempio di condizione di competizione potrebbe facilmente verificarsi nel sistema di ascensori quando viene
selezionato un piano da un passeggero. L'ascensore funziona con degli elenchi dei piani da visitare in ciascuna
direzione, verso l'alto e verso il basso. Quando l'ascensore arriva ad un piano, un thread di controllo rimuove quel
piano dall'elenco appropriato e richiama la successiva destinazione dall'elenco. Se l'elenco è vuoto, l'ascensore
cambia direzione, se l'altro elenco contiene dei piani, o si ferma, se entrambi gli elenchi sono vuoti. Un altro thread
di controllo è responsabile della collocazione delle richieste di piani nell'elenco appropriato, quando i passeggeri
selezionano i piani. Ogni thread esegue delle combinazioni di operazioni sull'elenco che non sono inerentemente
atomiche: ad esempio, verificando il successivo slot disponibile e riempiendolo. Se i thread inseriscono le loro
operazioni, potrebbero facilmente sovrascrivere lo stesso slot dell'elenco.
Il deadlock è una condizione in cui due thread di controllo sono entrambi bloccati, in attesa che l'altro intraprenda
la stessa azione. Ironicamente, il deadlock spesso nasce perché viene applicato qualche meccanismo di sincronizzazione
per evitare le condizioni di competizione.
L'esempio dell'ascensore relativo ad una condizione di competizione potrebbe facilmente provocare un caso relativamente
benigno di deadlock. Il thread di controllo dell'ascensore pensa che l'elenco sia vuoto e quindi non passa ad un altro
piano. Il thread della richiesta di piani pensa che l'ascensore sia impegnato a svuotare l'elenco e quindi non deve
avvisare l'ascensore di abbandonare lo stato di attesa.
Oltre alle questioni "fondamentali", esistono delle problematiche pratiche che devono essere esplicitamente risolte
nella progettazione del software simultaneo.
In una singola CPU, i meccanismi richiesti per simulare la simultaneità alternando le attività utilizzano cicli di CPU
che potrebbero altrimenti essere impegnati per l'applicazione stessa. D'altra parte, se il software deve attendere le
unità di I/O, ad esempio, i miglioramenti di prestazione consentiti dalla simultaneità potrebbero di gran lunga avere
maggior peso di qualsiasi spesa aggiunta.
Il software simultaneo richiede che i meccanismi di coordinazione e di controllo non siano necessari nelle applicazioni
a programmazione sequenziale. Essi rendono il software simultaneo più complesso ed aumentano le opportunità di errore.
I problemi nei sistemi simultanei sono inerentemente più difficili da diagnosticare a causa dei thread di controllo
multipli. D'altra parte, come è stato evidenziato in precedenza, quando le stesse forze esterne sono simultanee, il
software simultaneo che gestisce eventi diversi in modo indipendente può essere enormemente più semplice di un
programma sequenziale che deve sistemare gli eventi in ordine arbitrario.
Poiché molti fattori determinano l'inserimento dell'esecuzione di componenti simultanei, lo stesso software può
rispondere alla stessa sequenza di eventi in un ordine diverso. A seconda della progettazione, queste modifiche di
ordine possono produrre risultati differenti.
Il software applicativo può essere o non essere coinvolto nell'implementazione del controllo della simultaneità. Vi è
un intero spettro di possibilità, incluso, in ordine di coinvolgimento maggiore:
-
Le attività dell'applicazione possono essere interrotte in qualsiasi momento dal sistema operativo (multitasking
preventivo).
-
Le attività dell'applicazione possono definire delle unità atomiche di elaborazione (sezioni critiche) che non
devono essere interrotte, e devono informare il sistema operativo quando vengono utilizzate.
-
Le attività dell'applicazione possono decidere quando rilasciare il controllo della CPU ad altre attività
(multitasking collaborativo).
-
Il software applicativo prende completa responsabilità della pianificazione e del controllo dell'esecuzione di
varie attività.
Queste possibilità non sono un insieme esaustivo né si escludono a vicenda. In un dato sistema può essere utilizzata
una loro combinazione.
Un errore comune nella progettazione di sistemi simultanei è di selezionare troppo presto nel processo di progettazione
i meccanismi specifici da utilizzare per la simultaneità. Ciascun meccanismo comporta determinati vantaggi e svantaggi
e la scelta del "miglior" meccanismo per una particolare situazione viene spesso determinata da sottili compromessi.
Prima viene scelto il meccanismo, meno informazioni si hanno su cui basare la scelta. La scelta del meccanismo tende
anche a ridurre la flessibilità e l'adattabilità della progettazione nei confronti delle diverse situazioni.
Come per la maggior parte delle complesse attività di progettazione, la simultaneità si comprende meglio utilizzando
più livelli di astrazione. Innanzitutto i requisiti funzionali del sistema devono essere compresi in termini di
comportamento desiderato. In secondo luogo devono essere esplorati i possibili ruoli per la simultaneità. Ciò viene
svolto al meglio utilizzando l'astrazione dei thread senza impegnarsi in una particolare implementazione. La selezione
finale dei meccanismi per la realizzazione della simultaneità deve essere restare in sospeso fino a quando possibile,
per consentire una ottimizzazione delle prestazioni e la flessibilità di distribuire componenti in modo differente per
le varie configurazioni del prodotto.
La "distanza concettuale" fra il dominio del problema (ad es. un sistema di ascensori) e il dominio della soluzione (i
costrutti software) resta una delle difficoltà maggiori nella progettazione del sistema. I "formalismi visivi" sono
estremamente utili per la comprensione e la comunicazione di idee complesse come il comportamento simultaneo e, in
effetti, per colmare quella lacuna concettuale. Tool di comprovata validità per questo genere di problemi sono:
-
diagrammi di moduli per visualizzare i componenti che agiscono simultaneamente;
-
thread temporali per la visualizzazione di attività simultanee e interattive (che possono essere ortogonali
rispetto ai componenti);
-
diagrammi di sequenza per la visualizzazione delle interazioni fra i componenti;
-
diagrammi di transizione dello stato per la definizione degli stati e dei comportamenti dei componenti che
dipendono dagli stati.
Per progettare un sistema software simultaneo è necessario combinare i blocchi di costruzione del software (procedure e
strutture di dati) con quelli della simultaneità (thread di controllo). Si è discusso del concetto di attività
simultanea ma i sistemi non si costruiscono dalle attività. I sistemi si creano dai componenti e nella fatti specie i
sistemi simultanei di costituiscono con componenti simultanei. Considerati autonomamente, né le procedure né le
strutture di dati né i thread di controllo creano modelli naturali per i componenti simultanei ma gli oggetti sembrano
un modo molto naturale di combinare tutti questi elementi necessari in un unico pacchetto.
Un oggetto crea un pacchetto con le procedure e le strutture di dati in un componente coesivo con un proprio stato e un
proprio comportamento. Incapsula l'implementazione specifica dello stato e del comportamento e definisce le interfacce
tramite le quali altri oggetti o software possono interagire con l'oggetto. Gli oggetti in genere modellano entità o
concetti del mondo reale ed interagiscono con gli altri oggetti mediante lo scambio di messaggi. Sono ben accetti da
molti come il miglior modo per costruire sistemi complessi.
Figura 4: Un semplice insieme di oggetti per il sistema di ascensori.
Si consideri un modello di oggetto per il sistema di ascensori. Un oggetto stazione di chiamata ad ogni piano monitora
i pulsanti di chiama verso l'alto e verso il basso di quel piano. Quando un passeggero ipotetico preme un pulsante,
l'oggetto stazione di chiamata risponde inviando un messaggio ad un oggetto dispatcher degli ascensori, che sceglie
l'ascensore con tutta probabilità fornisce il servizio più veloce, informa l'ascensore e conferma la chiamata. Ogni
oggetto ascensore controlla simultaneamente ed in modo indipendente la sua controparte fisica di ascensore, rispondendo
alle selezioni di piano dei passengeri e alle chiamate del dispatcher.
La simultaneità può prendere due forme in questo tipo di modello di oggetto. La simultaneità fra oggetti si evince
quando due o più oggetti eseguono attività in modo indipendente tramite thread di controllo separati. La simultaneità
fra oggetti nasce quando più thread di controllo sono attivi un un unico oggetto. Nella maggioranza dei linguaggi
object-oriented di oggi gli oggetti sono "passivi," non disponendo di propri thread di controllo. Il thread di
controllo deve essere fornito da un ambiente esterno. Più comunemente, l'ambiente è un processo OS standard creato per
eseguire un "programma" object-oriented scritto in un linguaggio come C++ o Smalltalk. Se il sistema operativo (OS)
supporta il multi-threading, più thread possono essere attivi nello stesso oggetto o in oggetti differenti.
Nella figura riportata di seguito, gli oggetti passivi sono rappresentati dagli elementi circolari. L'area interna in
ombra di ciascun oggetto sono le informazioni sullo stato, e l'anello esterno segmentato è l'insieme di procedure
(metodi) che definisce il comportamento dell'oggetto.

Figura 5: Illustrazione dell'interazione degli oggetti.
La simultaneità fra oggetti porta con sé tutte le sfide del software simultaneo, ad esempio la possibilità di
condizioni di competizione quando più thread di controllo hanno accesso allo stesso spazio di memoria (in questo caso i
dati incapsulati nell'oggetto). Si potrebbe pensare che l'incapsulamento dei dati fornisca la soluzione a questa
problematica. Il problema, naturalmente, è che l'oggetto non incapsula il thread di controllo. Sebbene la simultaneità
fra oggetti evita gran parte di queste problematiche, esiste ancora un fastidioso problema. Affinché due oggetti
simultanei possano interagire scambiandosi messaggi, almeno due thread di controllo devono gestire il messaggio e
devono accedere allo stesso spazio di memoria per poterlo trasmettere. Un problema correlato (ma ancora più difficile)
è quello della distribuzione degli oggetti fra diversi processi o anche processori. I messaggi fra gli oggetti di
processi differenti richiedono il supporto per la comunicazione fra processi e in genere richiedono che il messaggio
venga codificato e decodificato in dati che possono essere trasmessi oltre i confini del processo.
Naturalmente nessuno di questi problemi è insormontabile. Infatti, come accennato nella sezione precedente, ogni
sistema simultaneo deve gestirli e quindi esistono delle soluzioni comprovate. Si tratta semplicemente del fatto che il
"controllo della simultaneità" comporta lavoro aggiuntivo e introduce ulteriori opportunità di errore. Inoltre adombra
l'essenza del problema applicativo. Per tutti questi motivi si tenta di ridurre al minimo la necessità che i
programmatori debbano occuparsi di queste problematiche esplicitamente. Un modo per farlo è di creare un ambiente
object-oriented con il supporto per il passaggio di messaggi fra oggetti simultanei (incluso il controllo della
simultaneità), e di ridurre al minimo o eliminare l'utilizzo di più thread di controllo in un singolo oggetto. In
effetti questo incapsula il thread di controllo insieme ai dati.
Gli oggetti con i propri thread di controllo vengono denominati "oggetti attivi". Per poter supportare la comunicazione
asincrona con altri oggetti attivi, ogni oggetto attivo viene fornito di coda di messaggi o di un "mailbox" (casella di
posta). Quando un oggetto viene creato, l'ambiente gli fornisce un proprio thread di controllo, che l'oggetto incapsula
fino a che cessa di esistere. Come per l'oggetto passivo, l'oggetto attivo è inattivo fino all'arrivo di un messaggi
dall'esterno. L'oggetto esegue il codice appropriato per elaborare il messaggio. I messaggi che arrivano mentre
l'oggetto è occupato, vengono collocati in una coda nella casella di posta. Quando l'oggetto termina l'elaborazione del
messaggio in corso, torna per recuperare il messaggio successivo in attesa nella casella di posta oppure attende un
nuovo arrivo. Dei buoni candidati come oggetti attivo nel sistema di ascensori sono gli ascensori stessi, le stazioni
di chiamata su ogni piano e il dispatcher.
In base al tipo di implementazione, gli oggetti attivi possono essere resi piuttosto efficienti. In qualche modo, però,
comportano più spesa dell'oggetto passivo. Quindi, poiché non tutte le operazioni devono essere simultanee, è cosa
comune mischiare gli oggetti passivi e attivi nello stesso sistema. A causa degli loro diversi stili di comunicazione,
è difficile renderli come pari, ma un oggetto attivo è un ambiente ideale per gli oggetti passivi, in sostituzione del
processo OS utilizzato prima. Infatti, se l'oggetto attivo delega tutto il lavoro agli oggetti passivi,
fondamentalmente equivale ad un processo OS o ad un thread con infrastrutture di comunicazione fra processi. Oggetti
attivi più interessanti, tuttavia, dispongono di un proprio comportamento per eseguire parte del lavoro, delegando le
altre parti agli oggetti passivi.
Figura 6: Un oggetto 'attivo' fornisce un ambiente per le classi passive
Dei buoni candidati come oggetti passivi all'interno di un oggetto attivo ascensore sono l'elenco dei piani ai quali
l'ascensore si deve fermare salendo ed un altro elenco relativo a quando scende. L'ascensore deve essere in grado di
chiedere all'elenco la fermata successiva, aggiungere nuove fermate all'elenco e rimuovere quelle che sono state
espletate.
Poiché i sistemi complessi vengono quasi sempre costruiti con diversi livelli di sottosistemi, per poi giungere ai
componenti, diventa una estensione naturale per il modello di oggetto attivo consentire agli oggetti attivi di
contenere altri oggetti attivi.
Anche se un oggetto attivo con thread singolo non supporta una vera simultaneità fra oggetti, delegare il lavoro agli
oggetti attivi contenuti è un ragionevole sostituto per molte applicazioni. Conserva l'importante vantaggio di un
completo incapsulamento di stato, comportamento e thread di controllo sulla base di uno per oggetto, che semplifica le
problematiche di controllo della simultaneità.
Figura 7: Il sistema di ascensori con oggetti attivi nidificati
Si consideri, ad esempio, il sistema di ascensori parziale descritto prima. Ogni ascensore ha delle porte, un argano ed
un pannello di controllo. Ognuno di questi componenti è ben modellato da un oggetti attivo simultaneo, in cui l'oggetto
porte controlla l'apertura e la chiusura delle porte dell'ascensore, l'oggetto argano controlla la posizione
dell'ascensore mediante l'argano meccanico, e l'oggetto pannello di controllo monitora i pulsanti di selezione dei
piani e quelli dell'apertura/chiusura delle porte. L'incapsulamento dei thread di controllo simultanei come oggetti
attivi porta ad un software molto più semplice di quello che si otterrebbe se tutto questo comportamento fosse gestito
da un unico thread di controllo.
Come è stato discusso nelle condizioni di competizione, perché un sistema agisca in modo corretto e prevedibile,
determinate operazioni dipendenti dallo stato devono essere atomiche.
Affinché un oggetto agisca in modo appropriato, è certamente necessario che il suo stato sia internamente coerente
prima e dopo l'elaborazione di qualsiasi messaggio. Durante l'elaborazione di un messaggio, lo stato dell'oggetto
potrebbe trovarsi in una condizione transitoria e essere indeterminato, poiché le operazioni potrebbero essere solo
parzialmente complete.
Se un oggetto porta sempre a termine la sua risposta ad un messaggio prima di rispondere ad un altro, la condizione
transitoria non è un problema. Anche interrompere un oggetto per eseguirne un altro non causa problemi perché l'oggetto
effettua un rigido incapsulamento del suo stato. (Parlando apertamente, questo non è del tutto vero, e verrà trattato
in seguito).
Qualsiasi circostanza in cui un oggetto interrompe l'elaborazione di un messaggio per elaborarne un altro apre la
possibilità alle condizioni di competizione e quindi richiede l'utilizzo di controlli di simultaneità. Questo, a sua
volta, comporta la possibilità di un deadlock.
La progettazione simultanea in genere è più semplice, quindi, se gli oggetti elaborano ogni messaggio fino al
completamento, prima di accettarne un altro. Questo comportamento è implicito nella particolare forma di modello di
oggetto attivo che è stato presentato.
La problematica dello stato coerente può manifestarsi in due diverse forme, nei sistemi simultanei, e sono forse più
semplici da comprendere in termini di sistemi simultanei object-oriented. La prima forma è quella già discussa. Se lo
stato di un singolo oggetto (passivo o attivo) è accessibile a più di un thread di controllo, le operazioni atomiche
devono essere protette dalla naturale atomicità delle operazioni CPU elementari o da un meccanismo di controllo della
simultaneità.
La seconda forma della problematica relativa allo stato coerente è forse molto più sottile. Se più di un oggetto
(attivo o passivo) contiene le stesse informazioni di stato, gli oggetti inevitabilmente saranno in disaccordo sullo
stato per almeno dei brevi intervalli di tempo. In una progettazione scarsa possono essere in disaccordo per
periodi più lunghi, anche per sempre. Questa manifestazione di stato incoerente può essere considerata un "duale"
matematico per l'altra forma.
Ad esempio, il sistema di controllo del movimento dell'ascensore (l'argano) deve garantire che le porte vengano chiuse
e non possano essere aperte, prima di far muovere l'ascensore. Una progettazione senza le protezioni appropriate
potrebbe consentire l'apertura delle porte in risposta alla pressione, da parte del passeggero, del pulsante di
apertura porte proprio quanto l'ascensore inizia a muoversi.
Potrebbe sembrare che una semplice soluzione a questo problema sia consentire alle informazioni di stato di risiedere
in un unico oggetto. Anche se questo può essere utile, può anche avere un impatto dannoso sulle prestazioni,
particolarmente in un sistema distribuito. Inoltre, non è una soluzione infallibile. Anche se un solo oggetto contiene
determinate informazioni di stato, finché altri oggetti simultanei effettuano decisioni in base a quello stato in un
determinato momento, le modifiche allo stato possono invalidare le decisioni di altri oggetti.
Non esiste una soluzione magica al problema dello stato coerente. Tutte le soluzioni pratiche richiedono
l'identificazione di operazioni atomiche e di proteggerle con qualche genere di meccanismo di sincronizzazione che
blocca l'accesso simultaneo per brevi periodi di tempo tollerabili. Questa "tollerabilità" dipende molto dal contesto.
Potrebbe essere il periodo di tempo che impiega la CPU a memorizzare tutti i byte in un numero di punto mobile o il
lasso di tempo che l'ascensore impiega per arrivare alla successiva fermata.
Nei sistemi real-time RUP consiglia l'utilizzo di capsule per
rappresentare gli oggetti attivi. Le capsule dispongono di semantica solida per semplificare la modellazione della
simultaneità:
-
utilizzano la comunicazione asincrona basata sui messaggi mediante porte che utilizzano protocolli ben definiti;
-
utilizzano la semantica run-to-completion (esecuzione fino al completamento) per l'elaborazione dei messaggi;
-
incapsulano oggetti passivi (garantendo che l'interferenza fra i thread non possa verificarsi).
|