Monitor

I semafori sono estremamente potenti e flessibili ma presentano alcune difficoltà:

  • La sincronizzazione è gestita in modo decentralizzato dai singoli thread. Se il programmatore non organizza bene le funzioni di sincronizzazione la manutenzione del codice può risultare difficoltosa
  • La gestione di mutex e sincronizzazione diventa complessa quando i thread devono attendere per condizioni non facilmente codificabili tramite un singolo semaforo. Ad esempio, nei lettori-scrittori abbiamo visto che viene eseguita un’attesa dentro una sezione critica.

I Monitor sono un costrutto linguistico [Tony Hoare ’74] che permette di risolvere i due problemi discussi sopra. Da un lato costringe il programmatore a centralizzare tutta la sincronizzazione in un un unico punto del programma (il monitor); dall’altro permette di gestire in modo molto efficace la mutua esclusione, soprattutto nel caso un thread debba attendere su condizione complesse.

I Monitor ricordano le classi della programmazione ad oggetti, in cui procedure e dati sono incapsulati in un’unità modulare: i thread non possono accedere direttamente ai dati ma devono utilizzare le procedure del Monitor.

I Monitor realizzano i seguenti meccanismi di sincronizzazione:

  1. Le procedure del monitor vengono eseguite in mutua esclusione;
  2. I Monitor forniscono delle variabili speciali dette condition sulle quali sono possibili due operazioni:
    • wait: il thread si mette in attesa sulla coda relativa alla condition. Il mutex del Monitor viene automaticamente rilasciato in modo da permettere ad altri thread di invocare le procedure del Monitor;
    • signal o notify: riattiva il primo thread in attesa sulla coda relativa alla condition. Se non ci sono thread in attesa non ha nessun effetto. Il thread viene eseguito immediatamente nel caso della signal o viene messo in attesa del mutex nel caso della notify (vedere sotto per una descrizione più approfondita).

Le condition sono simili ai semafori ma non c’è un valore: sono come semafori sempre rossi in quanto la wait è sempre bloccante e la signal non fa nulla se non ci sono thread in coda.

Differenza tra signal e notify

Quando viene eseguita una signal o notify e viene sbloccato un thread è necessario gestire la presenza simultanea dei due thread nel Monitor. Ci sono due possibilità:

  1. signal: Il thread che viene sbloccato dalla signal va subito in esecuzione nel Monitor mentre il thread che ha eseguito la signal attende, su una coda prioritaria, che il thread sbloccato esca dal Monitor;
  2. notify: Il thread che viene sbloccato dalla notify si mette in coda per ri-accedere al Monitor mentre il thread che ha eseguito la notify prosegue la sua esecuzione.

La differenza fondamentale è che con la signal siamo sicuri che il thread sbloccato verrà eseguito immediatamente mentre con la notify verrà eseguito in base allo scheduler. Questo significa che, con la notify, tra lo sbloccaggio e l’effettiva esecuzione potrebbero essere eseguiti altri thread cambiando eventualmente lo stato del Monitor. Questo comporta una diversità nel modo in cui si programma il Monitor che illustriamo qui sotto.

Produttore-Consumatore

Rivediamo ancora una volta l’esempio classico del produttore-consumatore. Avevamo provato a realizzare la sincronizzazione utilizzando un contatore di celle libere ma questo aveva portato a interferenze che necessitavano una sezione critica. I Monitor ci permettono di risolvere facilmente questo problema consentendo, inoltre, di mettere in attesa i thread evitando la tecnica del busy-waiting.

Per prima cosa dobbiamo riscrivere produttore e consumatore in modo che la sincronizzazione sia centralizzata, in stile object-oriented. Chiamiamo pc il Monitor produttore-consumatore. Tale Monitor ha due procedure riempi e svuota che scrivono e leggono dal buffer condiviso. Il codice risultante è il seguente:

Tutta la sincronizzazione avviene quindi nel Monitor che definiamo come segue:

Analizziamo la procedura riempi: Per poter scrivere nel buffer è necessario che ci sia una cella vuota. Quindi l’attesa avviene quando il contatore contiene il valore MAX tramite il seguente codice:

Il codice seguente scrive nel buffer. Poi viene incrementato il contatore e vengono sbloccati eventuali thread consumatori in attesa tramite il codice:

La procedura svuota è analoga.

Esempio di esecuzione

Consideriamo un’esecuzione con un consumatore C e due produttori P1 e P2. Il consumatore esegue pc.svuota() e poiché il contatore vale 0 si blocca sulla condition piene. Il mutex viene rilasciato e P1 può invocare pc.riempi(d). Lo stato è:

P1 invoca pc.riempi(d). Poiché il contatore vale 0 il buffer viene scritto, il contatore viene incrementato e viene eseguito piene.signal(). L’effetto è di sbloccare ed eseguire immediatamente il consumatore C in attesa. In questo modo siamo sicuri che ci sono celle da leggere in quanto nessun altro consumatore può aver letto tra la signal e l’esecuzione di C.

Il consumatore legge, decrementa il contatore che varrà nuovamente 0 ed esegue vuote.signal() che non fa nulla non essendoci thread in attesa su tale coda. C esce dal Monitor e viene eseguito P1 in quanto in attesa prioritaria rispetto a P2:

P1 di fatto esce immediatamente dal Monitor e viene eseguito P2 che scriverà nel buffer e aggiornerà il contatore:

Produttore-Consumatore con la notify

Lo schema precedente funziona perché i thread sbloccati dalla signal vengono eseguiti immediatamente. Infatti, se così non fosse, sbloccando, as esempio, un consumatore potrebbe accadere che il dato venga consumato da un altro thread consumatore prima che il thread sbloccato vada in esecuzione. Questo accade se utilizziamo la notify invece che la signal. In tale caso dobbiamo racchiudere la wait dentro un ciclo while in modo che se lo stato del Monitor si è modificato il thread possa bloccarsi nuovamente. In questo caso il codice diventa:

e

Filosofi a cena con raccolta atomica

Una delle soluzioni dello stallo nel problema dei filosofi a cena è la raccolta atomica delle bacchette. Se i filosofi raccolgono le due bacchette assieme non può verificarsi una situazione di attesa circolare. Abbiamo visto che realizzare questa soluzione con i semafori è difficoltosa, in quanto dovremmo rilasciare il mutex nel caso una delle bacchette non fosse disponibile. Avevamo detto che avremmo affrontato il problema con i Monitor.

Scriviamo lo schema del filosofo:

Come per il produttore-consumatore la sincronizzazione è centralizzata in un Monitor e abbiamo due procedure raccogli(i) e deposita(i) che raccolgono e depositano entrambe le bacchette del filosofo i-esimo.

Utilizziamo i monitor con notify che vedremo essere convenienti in questo caso.

I filosofi attendono se una delle bacchette non è disponibile. La wait è racchiusa in in while perché quando il filosofo viene svegliato deve verificare la presenza di entrambe le bacchette prima di proseguire. Infatti, in questo caso l’uso di signal non cambia l’esecuzione perché dobbiamo comunque ri-verificare la condizione di bloccaggio. Quando un filosofo deposita le bacchette notifica i filosofi a sinistra e a destra.

Per evitare il while è necessario utilizzare la signal al posto della notify ma si deve verificare la presenza di entrambe le bacchette prima di eseguire la signal sul filosofo vicino. Provare per esercizio.

Leave a Reply

Your email address will not be published. Required fields are marked *