MokaByte
Numero 04 - Gennaio 1997
|
|||
|
Programming |
||
Giovanni Puliti |
introduzione teorica alla programmazione multithreading in Java.. | ||
In
questo nummero di MokaByte inizia una serie di articoli che trattano della
programmazione multithread. Inizialmente questi articoli dovevano essere
pubblicati esclusivamente nella rubrica step-to-step e quindi erano stati
concepiti con un taglio strettamente pratico nella seppur breve tradizione
di tale rubrica.
Ci siamo resi conto invece che tale argomento è abbastanza corposo e che fra le novità introdotte con il linguaggio Java molte di queste sono legate al concetto di programmazione distribuita e quindi anche alla multiprogrammazione. Ci è sembrato quindi troppo riduttivo far partire questa nuova miniserie subito con un approccio pratico senza prma affrontare nessun aspetto teorico del problema. Per questo motivo ho scritto questa introduzione teorica ai threads e alle problematiche ad essi legate: cercherò quindi di esporre le caratteristiche teoriche generali della multiprogrammazione e degli aspetti specifici di quello che si può fare con Java. Questo articolo ha lo scopo di preparare il terreno per i neofiti di Java, in modo che possano seguire facilmente il minicorso pratico di Bertoncello. Per
iniziare ad affrontare un argmento così vasto e delicato, ho pensato
di partire un pò da lontano, cioé dai concetti basilari di
programmazione parallela per poi affrontare le specifiche del linguaggio
Java. Per capire completamente cosa sia un thread è bene iniziare
da quello che potrebbe essere il padre del multithrading, cioé il
multitasking. Molti di voi sicuramente conoscono approfonditamente tale
caratteristica che ormai quasi tutti i sistemi operativi esistenti implementano.
Conflitto
di priorità: spesso ai processi viene assegnata un valore di priorità
in base alla importanza che essi ricoprono nel sistema. Per la nostra trattazione
non ci preoccupiamo di come viene decisa l'importanza di un processo, ma
semplicemente si osserva, che fra due processi pronti per essere eseguiti,
viene in genere scelto quello più importante, cioé quello
a priorità più alta.
Infine in genere un processo ha a disposizione sempre un periodo di tempo massimo di occupazione continuativa della CPU, trascorso il quale esso viene messo a riposo per far spazio a processi più giovani. Il meccanismo del conteggio del tempo di elaborazione può essere utilizzato in accoppiata o subordinato al metodo delle priorità. Il
passo successivo per poter arrivare a parlare di thread consiste nel riconsiderare
la definizione vista precedentemente per affrontare il concetto di processo.
A
fronte dei numerosi vantaggi e potenzialità di un sistema a processi
uno degli inconvenienti maggiori è che il parallelismo (fittizio)
a tal punto viene gestito operando sui singoli processi che sono visti
come enitità indivisibili e non ulteriormente parallelizabili: è
impossibile ad esempio che uno di essi esegua due operazioni parallele
se non dando vita a due processi figli indipendenti fra loro. Inoltre
i processi figli di un unico genitore non sono in grado di condividere
variabili o scambiare informazioni se non tramite complicati meccanismi
di colloquio, per cui complessivamente si rende abbastanza difficile gestire
una struttura del genere.
Thread Un Thread è per definizione un pezzo di programma in cui le operazioni vengono svolte secondo la modalità procedurale classica: il grande vantaggio adesso risiede nel fatto che un processo può essere suddiviso in più thread i quali seguono la stessa logica del parallelismo fittizio dei processi in un sistema operativo multithread. Si ottiene quindi che un singolo processo può dar vita a sotto sezioni parallele che possono facilmente colloquiare fra loro in quanto condividono le stesse aree di memoria. Per far si che una classe sia threadizzata è sufficiente che essa possieda al suo interno una o più classi derivate dalla classe Thread oppure che implementi l'interfaccia Runnable. Per attivare i thread contenuti nella nostra classe è sufficiente lanciare i metodi run() che saranno riscritti col codice che vogliamo sia eseguito in parallelo. Senza dilungarci oltre vediamo subito come la JVM gestisce il ciclo della vita dei vari thread in esecuzione.(si osservi la Fig. 1)
Fig 1 si
parte dalla costruzione di un oggetto thread: in tale stato l'oggetto istaziato
da luogo ad una specie di fantasma in quanto il thread non è ancora
in fase di esecuzione e nessuna risorsa è stata riservata per lui.
Da questa fase solo dopo l'invocazione del metodo start() si ha
un effettivo passaggio di stato: tale metodo riserva le risorse necessarie
per il thread affinché possa essere eseguito correttamente.
Runnable: quando il thread diviene Runnable allora significa che è pronto per essere eseguito. Si noti la scelta della definzione di Runnable al posto di Running: infatti non è detto che il thread inizi immediatamente a lavorare sulla CPU, poiché il run time della JVM deve tener conto di tutti thread che sono nello stesso stato e concorrono per il processore. Si veda più avanti come lo scheduler gestisce tutti i thread concorrenti. Not Runnable: un thread finisce in questo stato per varie cause:
viene esplicitamente messo a riposo per un periodo prefissato attraverso l'invocazione del metodo sleep() il thread stesso si mette in attesa attraverso l'invocazione del metodo wait() di una qualche condizione di sistema. il
thread finisce in attesa di una risorsa di I/O non immediatamente disponibile.
Infine la condizione di fine thread corrispondente allo stato Dead: in tale fase finale le risorse allocate vengono rilasciate e tutto viene ristabilito. è lo stato finale del thread in cui esso cessa di esistere: prima che esso scompaia definitivamente dal sistema, si crea una fase intermedia detta zombie durante la quale il thread continua ad essere attivo per rilasciare le risorse allocate per la sua esecuzione. Un thread termina la sua vita o perché termina naturalmente la sua esecuzione, o perché viene invocato il metodo stop(): tale chiamata causa una cessazione immediata ed a volta troppo brusca del thread, ed in genere è preferibile predisporre un meccanismo che agisca sullo stato delle variabili del metodo run(). Come
si può notare tale ciclo di vita è del tutto equivalente
a quello di un processo in un sistema multitasking.
Priorità Come si è accennato precedentemente
i thread vengono fatti eseguire a rotazione sulla CPU come un meccanismo
detto di scheduling: il tipo di scheduling implementato dalla
JVM è semplice e si basa sulla differenza di priorità dei
vari thread.
In
genere la differenza di priorità fra i vari thread permette un usufrutto
equo della CPU, anche se è bene tener presente che il run time di
Java non effettua la cosidetta politica time-slice, attraverso la
quale è possibile gestire equamente anche thread a pari priorità.
In altre parole la JVM non effettua un cambio di contesto forzando la cessione
della CPU fra thread a pari priorità: si può verificare quindi
che un certo thread monopolizzi il processore non dando spazio ad altri
in attesa.
Programmazione Multithread e Sincronizzazione Uno dei problemi maggiori che
si presentano in applicazioni multithread è che spesso, proprio
a causa della loro natura (pseudo) parallela, si possono verificare incongruenze
in riferimento a risorse condivise.
Vediamo
come si può risolvere tale problema. La soluzione più semplice
si compone di due parti: la prima consiste nel predisporre un meccanismo
di sicurezza (spesso detto locking) in modo che nel momento in cui
un soggetto agisce sulla risorsa condivisa (ad esempio una area di memora)
sia impedito a chiunque altro di disturbare e/o di modificare la situazione
finché il primo occupante non abbia finito.
In linea di principio niente vieta di dichiarare syncronized pezzi di codice più piccoli di una funzione membro (ad esempio una singola variabile o un ciclo for), anche se una tale scelta va contro la filosofia della programmazione ad oggetti, rendendo difficile inoltre la fase di debug. E
importante notare che per ogni istanza di una classe che possiede metodi
sincronizzati, il run time crea un monitor separato.
Gruppi di Thread Come utima nota sulla programmazione
dei thread in Java vorrei analizzare un altro utile costrutto presente
in Java: i gruppi di thread.
Quindi non è necessario specificare obbligatoriamente un gruppo, anche se spesso questo può facilitare la gesitone della applicazione principale. Per gestire tutte le eventualità la classe Thread implementa i seguenti costruttori. public Thread(ThreadGroup group, Runnable target) public Thread(ThreadGroup group, String name) public Thread(ThreadGroup group, Runnable target, String name)Nel caso in cui si voglia ignorare la gestione dei gruppi penserà a tutto il sistema in maniera del tutto trasparente per il programmatore. Abbiamo
in questa sede esaminato i concetti fondamentali di thread e programmazione:
per chi si conosce gia' questi argomenti si richiederebbe forse una più
approfondita trattazione, ma in questa sede ho ritenuto opportuno semplicemente
esporre in breve i punti salienti per i neofiti. Adesso dovreste essere
capaci di seguire la serie step-to-step di Bertoncello
|
|
||
|
||
MokaByte ricerca
nuovi collaboratori
|
||
|