CAPITOLO 5:
MULTITASKING E MULTITHREADING
5.1: Multitasking
Il multitasking indica la possibilità di avere più
processi che vengono eseguiti simultaneamente su un singolo sistema. Il
sistema operativo deve gestire il processore e il tempo di esecuzione da
assegnare ad ogni programma che deve essere eseguito.
Gli scopi si vogliono raggiungere con l’uso del multitasking sono:
-
risposte rapide: viene data molta enfasi ad una risposta rapida agli input
provenienti dall’utente; una reazione pronta e visibile alle azioni dell’utente
è un fattore molto importante per dare la percezione di una maggior
velocità del sistema;
-
facilità di programmazione: i programmatori non dovrebbero avere
il bisogno di essere coscienti del fatto che il loro programma dovrà
funzionare in un ambiente caratterizzato da multitasking, poiché
la sua esecuzione dovrebbe procedere come se fosse l’unico processo a funzionare
sul sistema.
Si hanno due modalità nella gestione di più task contemporaneamente:
cooperative
e preemptive multitasking.
Si parla di cooperative multitasking quando tutti i processi
in esecuzione cooperano con il sistema operativo, condividendo il sistema
e le sue risorse; ogni programma in esecuzione ha un controllo completo
del sistema mentre è in esecuzione. Il sistema mette a disposizione
una routine, che i processi possono chiamare periodicamente (nel momento
in cui raggiungono un punto adeguato durante la loro esecuzione), che si
occupa di controllare se ci sia qualche processo in attesa di poter ottenere
l’uso della CPU: se tale controllo dà esito positivo, vengono controllate
le priorità e il processo in attesa con più alta priorità
viene mandato in esecuzione, fino al momento in cui anche questo non raggiunge
un punto adeguato e si ha un nuovo cambiamento di contesto. Questo approccio
richiede che tutti i programmi siano scritti in modo da chiamare frequentemente
la routine per il cambio di contesto, requisito inaccettabile quando si
devono effettuare grosse computazioni. Inoltre, nei sistemi che adottano
un cooperative multitasking, può risultare molto difficile scrivere
programmi ben formati per via delle enormi restrizioni che derivano dai
momenti imposti dal sistema per i cambi di contesto.
Nel preemptive multitasking i programmi non rilasciano volontariamente
il controllo del sistema, ma viene data loro l’impressione di avere un
accesso senza interruzioni al processore ed un suo completo controllo.
In un sistema di questo tipo si interrompe periodicamente un thread in
esecuzione per controllare se ce ne siano altri in attesa: in caso affermativo,
il sistema cambia automaticamente contesto. La scrittura dei programmi
non è condizionata dalla scelta di momenti più adatti degli
altri per i cambiamenti di contesto; se da un lato vengono rilasciate certe
restrizioni, dall’altro ne vengono imposte delle altre per evitare problemi
di inconsistenza dei dati nel caso in cui il programma venga interrotto
durante l’aggiornamento di una struttura dati: si rende necessaria l’introduzione
di meccanismi di locking delle risorse, in modo che soltanto il processo
in esecuzione possa accedere alle risorse del sistema. Tali meccanismi
devono essere il più trasparente possibile per l’utente.
In GEOS si adotta il secondo approccio, imponendo alle applicazioni
il rispetto di alcune regole per il buon funzionamento del sistema; le
applicazioni stesse, in gran parte sono isolate dall’ambiente multitasking.
5.2: Thread e multithreading
Un thread è una singola entità che viene eseguita
nel sistema; può essere di due tipi:
-
event-driven: si occupa di eseguire il codice di uno o più
oggetti e per questo ha associato una coda di messaggi; questo tipo di
thread riceve messaggi solo per gli oggetti da lui creati e rimane attivo
per il tempo necessario alla loro gestione; se non riceve mai messaggi,
non userà mai il processore;
-
procedural: si occupa di eseguire codice sequenziale, così
come funzioni o procedure C.
Quando un thread event-driven viene creato, viene associato ad una classe
per determinare i metodi da usare quando un messaggio è spedito
direttamente al thread; in questo senso si può dire che un thread
è l’istanza di una classe.
Si mantiene, anche, il concetto priorità, necessario per
determinare quale thread mandare in esecuzione dopo un cambiamento di contesto;
in particolare ogni thread ha:
-
una priorità di base: è un numero che indica la criticità
dell’esecuzione del thread; un valore basso implica che il thread è
critico per una veloce risposta del sistema, i valori più alti vengono
dati a quei thread meno time-critical. Per fornire una risposta ancora
più veloce la priorità di base viene decrementata di una
quota fissa nel momento in cui l’applicazione deve interagire con l’utente;
i valori tornano normali, quando l’utente decide di interagire con un’altra
applicazione. Tale numero cambia raramente.
-
una priorità corrente: è ottenuta a partire dalla
priorità base alla quale viene aggiunto un termine che tiene conto
dell’uso recente del processore da parte del thread. È questo il
parametro che si valuta nella scelta di un nuovo thread da eseguire dopo
un cambiamento di contesto: quello che, nell’insieme dei thread in attesa
di essere rimessi in esecuzione, ha priorità corrente più
bassa, è il prescelto; in caso di parità la scelta è
arbitraria.
L’uso di queste priorità avviene intelligentemente, evitando che
queste permettano a qualche thread di usare la CPU più del loro
quanto di tempo.
Ci sono due architetture per le applicazioni di GEOS:
-
quelle che prevedono un singolo thread;
-
quelle che prevedono l’uso di due thread (event-driven): uno per la gestione
della UI e l’altro per la gestione di tutte le altre funzionalità
dell’applicazione; in questo modo i messaggi inviati agli oggetti dell’interfaccia
possono essere gestiti senza aspettare che gli altri task dell’applicazione
siano terminati, rispondendo più prontamente ali input dell’utente.
Il problema da gestire con questa applicazioni è quello di mantenere
in linea l’esecuzione dei due thread con l’uso di semafori.
Ogni applicazione può definire più thread per raggiungere
vari scopi; un’applicazione con più di un thread può apparire
straordinariamente veloce rispetto alle altre.
5.3: Semafori
È necessario un modo per evitare che due thread accedano simultaneamente
alle stesse risorse di sistema: un meccanismo utile può essere quello
che prevede l’uso di semafori.
Un semaforo è una struttura dati sulla quale è possibile
effettuare tre operazioni di base:
-
inizializzazione: crea il semaforo, il cui stato iniziale è
unlocked, e gli dà un nome; l’uso di un semaforo prevede che questo
sia prima inizializzato, anche se questa è un’operazione condotta
dal sistema operativo in modo trasparente per il programmatore;
-
set (o operazione "P"): è l’operazione da compiere
perché un programma possa procedere. Se il semaforo è unlocked,
quest’operazione lo marca come locked e fa proseguire la computazione del
thread che l’ha invocata; se il semaforo è locked, il thread che
ha richiesto una risorsa già usata viene accodato insieme a tutti
gli altri thread in attesa che si liberi la risorsa di cui hanno bisogno;
-
reset (o operazione "V"): è l’operazione che sblocca
l’uso di una risorsa da parte di altri thread. Se ci sono altri thread
in attesa della risorsa, ne viene scelto uno cui passa il controllo dopo
aver impostato nuovamente il semaforo in uno stato locked; se non c’è
nessun tread in attesa della risorsa appena liberata, il semaforo relativo
viene impostato come unlocked, e quindi disponibile affinchè qualche
altro thread acceda alle risorse da lui controllate.
Queste tre operazioni impediscono a tutti i thread di avere conflitti per
l’accesso alle risorse condivise. Può essere utile pensare ad un
semaforo come ad un flag che un programma può impostare per indicare
che alcune risorse (quelle che sta usando) sono bloccate e non è
concesso a nessun altro programma di accedervi.
Dal momento che un solo thread per volta può accedere alle risorse
protette con il meccanismo di semafori, è importante osservare che
il thread che richiede il lock su una risorsa sia responsabile del suo
rilascio quando termina di utilizzarla.
Un problema strettamente interconnesso con la sincronizzazione degli
accessi e la gestione del meccanismo di locking è quello dei deadlock:
ossia situazioni per cui un thread A richiede il lock per una risorsa già
bloccata dal thread B, il quale, a sua volta, richiede un lock per una
risorsa bloccata da A. In questi casi il thread A interrompe la sua esecuzione
in attesa che il thread B liberi una risorsa e il thread B interrompe la
sua in attesa che il thread A liberi un’altra risorsa.
Per evitare queste situazioni, esistono delle regole che, se rispettate,
garantiscono l’assenza di deadlock in un programma:
-
evitare di avere un thread che cerca di ottenere un lock su un semaforo
già bloccato da un altro thread;
-
nel caso in cui due o più semafori vengono bloccati dallo stesso
thread, dovrebbero sempre essere usati nell’ordine secondo cui sono stati
bloccati. I semafori sono spesso organizzati secondo un gerarchia nella
quale i più grossi (perché controllano l’accesso a più
risorse) sono in cima, mentre quelli più piccoli sono posti in basso:
un thread che cerchi di ottenere più semafori della stessa gerarchia,
deve procedere a bloccare le risorse partendo dai semafori posti più
in alto, scendendo via via verso quelli più in basso; nessun thread,
infatti, dovrebbe richiedere un semaforo che si trovi al di sopra di uno
già bloccato all’interno della gerarchia;
-
in alcune situazioni un semaforo potrebbe essere usato da due thread: ad
esempio, quando un thread si mette in attesa che un altro thread compia
un’azione specifica. In questo caso si dice che il primo thread si blocca
sul secondo. In questo ambito è necessario evitare che succeda,
tra gli stessi thread, il viceversa: questa è una palese situazione
di deadlock.
GEOS possiede parecchie risorse protette da semafori; dal momento che,
però, i programmi delle applicazioni accedono a tali risorse solo
attraverso le routine di libreria, il programmatore non ha necessità
di essere a conoscenza di questi semafori, in quanto le operazione necessarie
per la loro gestione sono effettuate da tali routine di libreria.