Monitor (sincronizzazione)

costrutto di sincronizzazione di un linguaggio di programmazione di alto livello

Un monitor, nella programmazione, è un costrutto di sincronizzazione di un linguaggio di alto livello. Un'istanza di un tipo monitor può essere utilizzata da due o più processi o thread per rendere mutuamente esclusivo l'accesso a risorse condivise. Il vantaggio nell'utilizzo del monitor deriva dal fatto che non si deve codificare esplicitamente alcun meccanismo per realizzare la mutua esclusione, giacché il monitor permette che un solo processo sia attivo al suo interno.

Caratteristiche modifica

Un monitor tipo è formato da:

  • variabili locali, i cui valori definiscono lo stato di un'istanza del tipo monitor;
  • un blocco d'inizializzazione o una procedura d'inizializzazione dei valori dei dati locali di un'istanza del tipo (detto anche costruttore).
  • un insieme di corpi di procedure o funzioni che realizzano le operazioni del tipo.

Le variabili locali sono dichiarate come private, ovvero sono accessibili solo dalle procedure del monitor. Quando un monitor viene istanziato, è avviato il blocco d'inizializzazione oppure dev'essere invocata la sua procedura d'inizializzazione, in modo analogo al modo in cui viene invocato un metodo costruttore di una classe di un linguaggio di programmazione ad oggetti quando viene istanziato un oggetto, oppure, effettivamente in quel modo, se il monitor è una classe di un oggetto. Un processo entra in un monitor invocando una delle sue procedure. All'interno del monitor può essere attivo un solo processo per volta, sicché, quando un processo invoca una procedura, la richiesta viene accodata e soddisfatta non appena il monitor è libero.

La struttura generica di un monitor:

   monitor nome_del_monitor {

      /* spazio per la dichiarazione delle variabili condivise */

      procedure nome_della_procedura_1 (parametro_1, ..., parametro_N) {
         /* codice del corpo della procedura */
      }
       . . .
      procedure nome_della_procedura_N (parametro_1, ..., parametro_N) {
         /* codice del corpo della procedura */
      }

      {
         /* codice del blocco d'inizializzazione */
      }
   }

Un esempio di monitor:

monitor conto_corrente {
  double saldo:= 0.0
  
  procedure preleva(double importo) {
    if importo < 0.0 then error "L'importo del prelievo deve essere un numero positivo"
    else if saldo < importo then error "Fondi insufficienti per il prelievo"
    else saldo:= saldo - importo
  }
  
  procedure versa(double importo) {
    if importo < 0.0 then error "L'importo del versamento deve essere un numero positivo"
    else saldo:= saldo + importo
  }
 
  double function saldo() {
    return saldo
  }
}

Nell'esempio, si ha la garanzia che la transazione aggiorni correttamente il saldo del conto corrente.

Variabili condizionali modifica

Senza ulteriori meccanismi, il rigido vincolo di mutua esclusione del costrutto non permetterebbe sincronizzazioni particolari. Invece, le variabili condizionali, di tipo condition, che possono essere definite tra le altre variabili locali, e le procedure attendi(condition x) e notifica(condition x), le uniche a poter essere invocate su di esse, forniscono ad un programmatore gli strumenti necessari per scrivere un proprio schema di sincronizzazione. L'operazione attendi(), applicata ad una variabile condizionale, permette di sospendere un processo che occupa il monitor, facendo in modo che il processo sparisca temporaneamente dal monitor e venga posto in una coda d'attesa per quella variabile condizionale, dando così via libera ad un nuovo processo che desidera entrare nel monitor oppure ad un altro processo pronto, presso una variabile condizionale, a riprendere l'esecuzione. L'operazione notifica() risveglia esattamente un processo sospeso sulla variabile condizionale per cui è chiamata; questo processo riprende la propria esecuzione appena ha via libera. In ogni caso, quando non ci sono processi in attesa sulla variabile condizionale per cui è chiamata la notifica, non accade nulla. La notifica, in base alle sue politiche d'invio, può dar luogo a differenti evoluzioni del sistema, qui riassunte:

  • notifica urgente (signal urgent): avviene la riattivazione immediata di un processo in attesa sulla variabile per cui la notifica è chiamata, mentre il processo che compie la notifica viene accodato alla coda di attesa della stessa, per essere riattivato quando il processo risvegliato lascia il monitor o si mette in attesa su una variabile condizionale; soluzione sostenuta da C. A. R. Hoare;
  • notifica e ritorna (signal and return): il processo, dopo aver compiuto la notifica, esce immediatamente dal monitor;
  • notifica e continua (signal and continue): il processo, dopo aver compiuto la notifica, continua la sua esecuzione; solo dopo che questo ha lasciato il monitor o si è messo in attesa su una variabile condizionale, il processo in testa alla coda di attesa della variabile condizionale per cui è stata effettuata la notifica viene riattivato.

Il seguente è l'esempio di un monitor che usa le variabili condizionali per implementare un canale di comunicazione tra processi (per es. una pipe), che può memorizzare un solo intero per volta.

 monitor canale {
  int contenitore
  boolean pieno:= false
  condition spedito
  condition ricevuto

  function spedisci(int valore) {
    if pieno then attendi(ricevuto)
    contenitore:= valore
    pieno:= true
    notifica(spedito)
  }

  int function ricevi() {
    if not pieno then attendi(spedito)    
    pieno:= false
    notifica(ricevuto)
    return contenitore
  }
}

Proviamo a simulare una possibile esecuzione del monitor canale preso ad esempio. Supponiamo che un primo processo chieda al monitor di ricevere e si blocchi poiché il canale non è pieno; a questo punto, scompare dal monitor, in attesa del verificarsi della condizione 'spedito'. Supponiamo, inoltre, che altri tre processi chiedano di ricevere: vengono tutti accodati tra i processi da risvegliare sulla variabile condizionale spedito. Fintanto che non ci sono processi che inviano un intero sul canale, invocando spedisci(), i nostri quattro processi sono accodati in attesa di una spedizione. Solo nel caso in cui, finalmente, arriva un processo che richiede al monitor di spedire, ad esempio con spedisci(3), non essendoci altri processi che usano il monitor (i quattro processi accodati sulla variabile condizionale 'spedito' sono tutti invisibili), il monitor osserva che il contenitore è vuoto, lo riempie e notifica che il canale ha spedito dei dati. Questa notifica, a seconda della politica utilizzata, ha effetto sul processo in testa alla coda di attesa della variabile condizionale spedito.

Storia modifica

Inizialmente proposti da Per Brinch Hansen sulla base di precedenti lavori di C. A. R. Hoare. Lo stesso Hoare in seguito sviluppò la teoria dei monitor e dimostrò che, da un punto di vista della potenza espressiva, i monitor erano equivalenti ai semafori.

I monitor furono proposti inizialmente come modello teorico, ma furono in seguito adottati da molti linguaggi di programmazione concorrente. Alcuni esempi di linguaggi che consentono l'uso di monitor si possono citare:

Il linguaggio C# non ha i monitor come caratteristica primitiva del linguaggio, ma una funzione analoga viene fornita da una classe di libreria del framework .NET.

I monitor sono stati adottati da diversi sistemi operativi come meccanismo di controllo della concorrenza (sebbene i meccanismi primitivi si fondino in genere sul concetto di semaforo): un esempio è l'interfaccia pthreads di POSIX.

Bibliografia modifica

  • Monitors: an operating system structuring concept, C. A. R. Hoare - Communications of the ACM, v.17 n.10, p. 549-557, Oct. 1974 [1]
  • Monitor classification P.A. Buhr, M. Fortier, M.H. Coffin - ACM Computing Surveys (CSUR), 1995 [2]

Altri progetti modifica

  Portale Informatica: accedi alle voci di Wikipedia che trattano di informatica