Salta al contenuto
Note per Studenti Esercizio - Il barbiere che dorme con i semafori (eserciziario, esercizio 68)

Esercizio - Il barbiere che dorme con i semafori (eserciziario, esercizio 68)

Testo (eserciziario del corso di Sistemi Operativi, Ing. Informatica UniPD, esercizio 68). In un negozio di barbiere ci sono:

  • una sedia per lavorare (quella del barbiere);
  • nn sedie per far attendere i clienti.

Il barbiere di solito dorme; quando arriva un cliente, questi lo sveglia e si fa servire; se nel frattempo arrivano altri clienti, si accomodano sulle sedie oppure, se sono tutte occupate, se ne vanno. Implementare con semafori usando le variabili:

c
#define CHAIRS 5                 /* sedie per i clienti in attesa */
typedef int semaphore;
semaphore customers = 0;         /* clienti in attesa di servizio */
semaphore barbers   = 0;         /* barbieri in attesa di clienti (pronti a tagliare) */
semaphore mutex     = 1;         /* mutua esclusione per la variabile waiting */
int waiting = 0;                 /* clienti in attesa (non quelli in servizio) */

Teoria: Semafori, monitor e problemi classici di sincronizzazioneUn semaforo è un intero accessibile solo con due operazioni atomiche, wait (P: aspetta finché S > 0 e decrementa) e signal (V, in POSIX sem_post: incrementa); il semaforo contatore ha valori illimitati, quello binario vale 0 o 1; con valore iniziale 1 realizza la mutua esclusione, con valore iniziale 0 impone un ordine di esecuzione (S1 prima di S2); senza attesa attiva ogni semaforo ha una coda di processi sospesi (sleep e wakeup); i semafori si usano male con facilità (ordine sbagliato, omissioni); il monitor è un tipo di dato astratto in cui un solo processo alla volta è attivo e le variabili condizione x.wait()/x.signal() sospendono e risvegliano (in POSIX mutex più pthread_cond_t); le anomalie di liveness sono deadlock, starvation e inversione di priorità; i problemi classici sono produttore-consumatore a buffer limitato (mutex, vuote = n, piene = 0; l'ordine delle wait è importante), lettori-scrittori (rw_mutex, mutex, read_count; i lettori possono far morire di fame gli scrittori) e filosofi a cena (la soluzione ingenua con un semaforo per bacchetta va in deadlock; si evita limitando a n-1 filosofi, rompendo la simmetria o ordinando le risorse), con soluzioni in C (POSIX) e in Go (canali e sync).Semafori, monitor e problemi classici di sincronizzazione →.

Soluzione del corso

void barber(void) {                          void customer(void) {
    while (TRUE) {                               wait(&mutex);
        wait(&customers);                        if (waiting < CHAIRS) {
        wait(&mutex);                                waiting = waiting + 1;
        waiting = waiting - 1;                       signal(&customers);
        signal(&barbers);                            signal(&mutex);
        signal(&mutex);                              wait(&barbers);
        cut_hair();                                  get_haircut();
    }                                            } else {
}                                                    signal(&mutex);
                                                 }
                                             }

Come funziona.

  • customers conta i clienti in attesa: il barbiere fa wait(customers) e dorme se è 0; ogni cliente che si siede fa signal(customers) e quindi lo sveglia.
  • barbers serve a far incontrare cliente e barbiere: il barbiere fa signal(barbers) quando è pronto a tagliare a uno dei clienti; il cliente fa wait(barbers), e solo allora si fa tagliare i capelli.
  • waiting è una copia del numero di clienti seduti (i semafori non si possono leggere): serve a decidere se c'è posto. È protetta da mutex.

Perché i controlli e le modifiche sono fatti dentro mutex. Il cliente legge waiting e lo incrementa nella stessa sezione critica: due clienti non possono entrambi vedere "c'è un posto" quando ce n'è uno solo. Il barbiere decrementa waiting quando sceglie un cliente.

Sincronizzazione. Il cliente non tiene mutex mentre aspetta il barbiere (rilascia con signal(mutex) prima di wait(barbers)): altrimenti il barbiere, che deve prendere mutex per decrementare waiting, non potrebbe svegliarlo. Sarebbe un deadlock.

Il programma C seguente simula il negozio con 3 sedie e 8 clienti (arrivati a intervalli diversi); alla fine conta quanti sono stati serviti e quanti se ne sono andati.

barbiere.cc
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>

#define CHAIRS 3
#define CLIENTI 8

static sem_t customers, barbers, mutex;
static int waiting;                       /* clienti seduti in attesa */
static int serviti, persi;
static int fine;                          /* tutti i clienti sono passati */

static void *barber(void *arg)
{
    (void)arg;
    for (;;) {
        sem_wait(&customers);             /* dorme se nessuno e' in attesa */
        sem_wait(&mutex);
        if (fine && waiting == 0) {       /* risveglio finale */
            sem_post(&mutex);
            return NULL;
        }
        waiting--;
        sem_post(&barbers);               /* e' pronto per un cliente */
        sem_post(&mutex);
        usleep(2000);                     /* cut_hair() */
    }
}

static void *customer(void *arg)
{
    long id = (long)arg;
    usleep((useconds_t)(id * 500));       /* arrivi a intervalli diversi */
    sem_wait(&mutex);
    if (waiting < CHAIRS) {
        waiting++;
        sem_post(&customers);             /* sveglia il barbiere se dorme */
        sem_post(&mutex);
        sem_wait(&barbers);               /* aspetta il suo turno (senza tenere mutex) */
        sem_wait(&mutex);
        serviti++;
        sem_post(&mutex);
        printf("cliente %ld servito\n", id);
    } else {
        persi++;
        sem_post(&mutex);
        printf("cliente %ld se ne va (sedie piene)\n", id);
    }
    return NULL;
}

int main(void)
{
    pthread_t b, c[CLIENTI];
    sem_init(&customers, 0, 0);
    sem_init(&barbers, 0, 0);
    sem_init(&mutex, 0, 1);
    pthread_create(&b, NULL, barber, NULL);
    for (long i = 0; i < CLIENTI; i++)
        pthread_create(&c[i], NULL, customer, (void *)i);
    for (int i = 0; i < CLIENTI; i++)
        pthread_join(c[i], NULL);
    sem_wait(&mutex);
    fine = 1;
    sem_post(&mutex);
    sem_post(&customers);                 /* sveglia il barbiere per farlo terminare */
    pthread_join(b, NULL);
    printf("serviti %d, persi %d, totale %d\n", serviti, persi, serviti + persi);
    return 0;
}

Controlli: il totale serviti + persi è sempre uguale a CLIENTI (8). Il barbiere esce solo con fine vero e nessuno in attesa; la wait(customers) finale è sbloccata dalla sem_post del main.

Domande d'esame

Cosa succederebbe se il cliente non rilasciasse mutex prima di wait(barbers)? Traccia. Il barbiere, per sceglierlo, deve prendere mutex (per waiting--): ma il cliente lo tiene mentre aspetta barbers, che solo il barbiere può segnalare: attesa circolare, deadlock.

Versione ripasso

Esercizi su questo argomento

Teoria collegata