Concetto: Simultaneità
La simultaneità è la tendenza delle cose ad accadere nello stesso momento nel sistema. 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.
Relazioni
Descrizione principale
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.

Cos'è la simultaneità?

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.

Il diagramma è descritto nel contenuto.

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.

Perché si è interessati alla simultaneità?

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).

Cosa rende difficile il software simultaneo?

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.

Esempio di un sistema real-time simultaneo: un sistema di ascensori

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.


Diagramma descritto nel contenuto.
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.

Simultaneità come strategia di semplificazione

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.

Astrazione della simultaneità

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.

Il diagramma è descritto nel contenuto.

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.

Realizzazione della simultaneità: meccanismi

Gestione dei thread di controllo

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

Multitasking

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.

Multithread

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.

Elaborazione multipla

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.

Problematiche fondamentali relative al software simultaneo

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.

Interazione asincrona e sincrona

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.

Contesa delle risorse condivise

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.

Condizioni di competizione: la questione dello stato coerente

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.

Deadlock

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.

Altre questioni pratiche

Oltre alle questioni "fondamentali", esistono delle problematiche pratiche che devono essere esplicitamente risolte nella progettazione del software simultaneo.

Compromessi di prestazione

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.

Compromessi di complessità

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.

Non determinismo

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 ruolo del software applicativo nel controllo della simultaneità

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:

  1. Le attività dell'applicazione possono essere interrotte in qualsiasi momento dal sistema operativo (multitasking preventivo).
  2. 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.
  3. Le attività dell'applicazione possono decidere quando rilasciare il controllo della CPU ad altre attività (multitasking collaborativo).
  4. 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.

Astrazione della simultaneità

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.

Oggetti come componenti simultanei

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.

Il diagramma è descritto nel contenuto.

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.

Diagramma descritto nel contenuto.
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.

Il modello di oggetto attivo

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.

Il diagramma è descritto nel contenuto.

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à.

Il diagramma è descritto nel contenuto.

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.

La questione dello 'stato coerente' negli oggetti

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.

Sistemi real-time

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).