Introduzione
Definiremo alcuni concetti relativi all'accesso di risorse condivise, considereremo alcune situazioni critiche che possono presentarsi e analizzeremo le soluzioni esistenti in letteratura.
Processo
Definiamo in questa sede un Processo come una sequenza di istruzioni svolte da un calcolatore per eseguire un programma P. Un processore è un dispositivo in grado di eseguire tali azioni. Un processore può alternare nel tempo l'esecuzione di istruzioni di programmi diversi, viceversa l'esecuzione di un programma può essere sospesa e ripresa più volte, anche su processori diversi. Il processo può dunque essere complessivamente svolto in tempi diversi e su processori diversi.
Nota L'interruzione di un processo è un'operazione di solito in carico al sistema operativo, ma per i linguaggi interpretati o semi interpretati può anche essere gestito indirettamente dall'ambiente di run-time in cui avviene l'esecuzione.
|
Risorse
- potrebbe essere necessario impedire che uno dei due inizi ad usarla prima che l'altro abbia finito, si pensi banalmente ad una stampante, in cui non è possibile stampare righe da un documento intervallate da righe di un altro documento
- ogni processo presume di conoscere lo stato della risorsa quando inizia ad operarvi, ma se più processi vi operano simultaneamente questo non è più vero
Interazioni tra processi
Problema della mutua esclusione
- il processo continua ad impegnare il processore in attesa della risorsa (attesa attiva)
- il processo viene sospeso sin quando la risorsa non diviene disponibile (attesa passiva)
Interazione diretta
A livello di linguaggio macchina, il nucleo di questi meccanismi risiede in istruzioni di test and set, cmpxchg o xchg su cui viene applicato un lock a livello di bus per impedirne la concorrenza. |
Produttore consumatore
- mutex sono oggetti in generale che consentono l'accesso esclusivo a risorse
- semaforo è un oggetto che gestisce l'accesso condiviso ad una risorsa, non necessariamente esclusivo, gestendo una coda di richieste da parte di più processi
object x= new object(); //rappresenta simbolicamente la risorsa condivisa lock (x) { // istruzioni che accedono alla risorsa associata ad x
// solo un processo alla volta può eseguire questo codice o altre
// sezioni simili incorporate in un lock(x)
}
- Mutex consente di ottenere l'accesso esclusivo ad una risorsa. Si avvisa che esistono anche altri metodi di Mutex che consentono, tramite un nome, di accedere a mutex esterni all'applicazione, tramite meccanismi forniti dal sistema operativo ospitante.
- Mutex(bool initiallyOwned) costruisce il mutex, eventualmente auto-assegnandoselo se initiallyOwned è true
- bool waitOne() acquisice un blocco esclusivo sul mutex. L'esecuzione si blocca, anche indefinitamente, sin quando non è acquisito il blocco. Il metodo restituisce sempre true.
- bool waitOne(int milliseconds) simile al precedente, ma con un limite di tempo per l'acquisizione; se il lock non è acquisito entro il tempo prefissato, la funzione restituisce false
- void ReleaseMutex() rilascia il mutex
- Semaphore limita il numero di processi che accedono simultaneamente ad una risorsa
- Semaphore(int nInitial, int nMax) crea un semaforo per l'accesso ad una risorsa a cui potranno accedere massimo nMax processi e inizialmente si assume che nInitial stiano già accedendo ad esso
- bool waitOne(), bool waitOne(int milliSeconds) come per il Mutex
- int Release() rilascia il semaforo e restituisce il numero di utilizzatori prima dell'istruzione Release
- int Release(nTime) rilascia il semaforo nTime volte, è equivalente a chiamare Release() nTime volte
- SemaphoreSlim simile a Semaphore ma non utilizza il kernel di sistema, quindi è più efficiente, ma funziona a patto che sia usato all'interno di uno stesso programma. Non presenta le varianti "per nome" di Semaphore e del Mutex
- EventWaitHandle rappresenta un evento di sincronizzazione
- EventWaitHandle(bool initialState, EventResetMode tipoReset) crea un evento con uno stato iniziale (risorsa disponibile=true o non disponibile=false), ed un tipo di reset. Se il tipo reset è autoReset, l'evento diventa non disponibile non appena un processo ne ottiene l'accesso (con waitOne ad esempio). Se il tipo reset è manualReset, occorre chiamare i metodi Reset e Set per cambiarne lo stato.
- bool Reset() imposta l'evento come non disponibile (e bloccare i vari processi), restituisce true se riesce
- bool Set() imposta l'evento come disponibile (e sbloccare uno o più processi)
- bool waitOne(), bool waitOne(int milliSeconds) come per il Mutex
- bool SignalAndWait(WaitHandle toSignal, WaitHandle toWait) rilascia il lock su un evento (toSignal) e si mette in attesa di un altro (toWait), sin quando non viene rilasciato.
- Monitor garantisce l'accesso ad una risorsa condivisa, ed è simile per certi versi all'istruzione lock, ma consente di riferirsi anche a lock di sistema su oggetti esterni al programma
- static void Enter(object o) rimane in attesa della risorsa o, sin quando non diviene disponibile
- static void Exit(object o) rilascia la risorsa collegata
- static void Wait(object o) rilascia la risorsa e si mette in attesa della risorsa stessa (che avviene tramite una Pulse)
- static void Pulse(object O) rilascia nuovamente la risorsa al processo che l'aveva rilasciata con Wait.
Polling vs Event Driven I metodi considerati si intendono essere tutti "event driven" ossia il thread che li esegue rimane bloccato e in attesa "passiva", ossia non impegna il processore, sin quando il lock non viene acquisito. Questo meccanismo si contrappone al meccanismo di "polling" in cui il processo interroga, in maniera attiva, eventualmente ad intervalli di tempo, lo stato di una risorsa, sin quando questa non divenga disponibile. |
C# Produttore Consumatore- semaphore
public class Producer { static List<T> data = new List<T>(); static object queue= new Semaphore(initialCount:1, maximumCount:10); void produce() { //qualche elaborazione lock(data){ data.Add(risultato); } queue.Release(); } void consume() { queue.WaitOne(); object dataToProcess; lock(data){ dataToProcess = data[0]; data.RemoveAt(0); } //elaborazione di dataToProcess ... } }
Task.Run ( () => { while(true) consume(); } );
C# Produttore Consumatore - senza semaphore
void consume() { object dataToProcess=null; lock(data){
if (data.Count>0){
dataToProcess = data[0]; data.RemoveAt(0); }
if (dataToProcess!=null) { //elaborazione di dataToProcess
} ... }
Task.Run ( () => { while(true) { consume(); await Task.Delay(5000); } );
Nessun commento:
Posta un commento