Semafori POSIX

I semafori POSIX sono semafori contatori che permettono di gestire la sincronizzazione dei thread POSIX. Esistono altri meccanismi che, per mancanza di tempo, menzionano solamente:

  • Pthread Mutex: sono semafori binari che permettono di realizzare la sezione critica. Non assumono un valore intero e sono solamente verdi o rossi;
  • Pthread Condition: vengono utilizzate per realizzare i monitor in C. Vedremo i monitor nel linguaggio Java.

I semafori POSIX si utilizzano tramite le seguenti strutture dati e funzioni, definite nella libreria semaphore.h:

  • sem_t sem_name: dichiara una variabile di tipo semaforo;
  • int sem_init(sem_t *sem, int pshared, unsigned int value) inizializza il semaforo sem al valore value. la variabile pshared indica se il semaforo è condiviso tra thread (uguale a 0) o processi (diverso da 0), lo useremo quindi sempre con 0.
  • int sem_wait(sem_t *sem) esegue una P(sem);
  • int sem_post(sem_t *sem) esegue una V(sem);
  • int sem_getvalue(sem_t *sem, int *val) legge il valore del semaforo e lo copia in val;
    ATTENZIONE: in alcune implementazioni il semaforo rosso è 0, in altre è negativo (e indica il numero di processi in attesa);
  • sem_destroy(sem_t *sem) elimina il semaforo. Da NON usare se ci sono processi in attesa sul semaforo (comportamento non specificato).
NOTA per chi ha Mac e NON utilizza docker: macOS non supporta i semafori ‘unnamed’ descritti qui sopra. Per poter utilizzare i semafori POSIX si devono usare i semafori con nome (che tra l’altro sono facilmente condivisibili tra processi, un po’ come le pipe con nome). Al posto della sem_init si deve usare la sem_open e al posto della sem_destroy si usano sem_close e sem_unlink. Ovviamente i semafori con nome si possono utilizzare anche in Linux.

Esercizio 1: Sezione Critica

Riprendiamo l’ultimo esercizio della volta scorsa:

Creare 2 thread che aggiornano ripetutamente (in un ciclo for) una variabile condivisa count per un numero elevato di volte (ad esempio 1000000). Stampare il valore finale per osservare eventuali incrementi perduti…

(Seguiva una nota sulle ottimizzazioni del compilatore. Vedere le dispense della volta scorsa per maggiori dettagli)

Aggiungere un semaforo mutex per risolvere le interferenze. È sufficiente aggiungere una variabile globale sem_t sem e, all’inizio e alla fine del main, l’inizializzazione e la rimozione del semaforo: sem_init(&sem,0,1) e sem_destroy(&sem). Infine, per proteggere la sezione critica, aggiungere sem_wait(&sem) e sem_post(&sem) prima e dopo la lettura/modifica di count. Notare l’esecuzione corretta al prezzo di una più bassa performance.

for (j=0;j<MAX;j++) { 
    sem_wait(&sem); 
    count++; 
    sem_post(&sem); 
}

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>   /* For SYS_xxx definitions */
#include <semaphore.h>
// codice dei thread. Notare che e' una funzione che prende 
// un puntatore e ritorna un puntatore (a void)

#define MAX 1000000
int contatore=0; // variabile globale condivisa da tutti i thread
sem_t mutex; // semaforo mutua esclusione

void * codice_thread(void * a) {
    pthread_t tid;
    int ptid,i;

    tid  = pthread_self();      // library tid
    ptid = syscall(SYS_gettid); // tid assegnato dal SO (funziona solo in Linux)
    printf("Sono il thread %lu (%i) del processo %i\n",tid,ptid,getpid());

    for(i=0;i<MAX;i++) {

        // sezione critica basta mettere una sem_wait prima di contatore++
        // e una sem_post dopo contatore++. Poiche' mutex è inizializzato
        // a 1 queta costruzione realizza una sezione critica dando
        // accesso a un thread alla volta.

        sem_wait(&mutex); // P
        contatore++; // variabile globale condivisa ==> INTERFERENZE!!!
        sem_post(&mutex); // V

    }
    pthread_exit(NULL);
}

int main() {
    pthread_t tid[2];
    int i,err;
    // create
    sem_init(&mutex,0,1); // inizializziamo a 1

    // crea i thread (ritorna 0 quando ha successo, vedere il man!)
    // - gli attributi sono quelli di default (il secondo parametro e' NULL)
    // - codice_thread e' il nome della funzione da eseguire
    // - non vegnono passati parametri (quarto parametro e' NULL)
    for (i=0;i<2;i++) {
        if (err=pthread_create(&tid[i],NULL,codice_thread,NULL)) {
            printf("errore create [%i]\n",err);
            exit(EXIT_FAILURE); }
    }
    // attende i thread. Non si legge il valore di ritorno (secondo parametro NULL)
    for (i=0;i<2;i++) {
        if (err=pthread_join(tid[i],NULL)) {
            printf("errore join [%i]\n",err);
            exit(EXIT_FAILURE); }
    }

    // destroy
    sem_destroy(&mutex);
    printf("I thread hanno terminato l'esecuzione correttamente. contatore=%d\n",
        contatore);
}

Soluzione usando i semafori con nome

NOTA: Nel caso si vogliano usare i semafori con nome (es. macOS senza docker) è necessario fare: mutex=sem_open("/mutex",O_CREAT,0644,1) e sem_close(mutex); sem_unlink("/mutex"); facendo attenzione che in questo caso mutex sarà un puntatore a sem_t (quindi anche la wait e la post andranno fatte su mutex e non su &mutex). Vedere la soluzione qui sotto:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>   /* For SYS_xxx definitions */
#include <semaphore.h>
#include <fcntl.h>
#include <errno.h>
// codice dei thread. Notare che e' una funzione che prende 
// un puntatore e ritorna un puntatore (a void)

#define MAX 1000000
#define SEM "/mutex"  // il nome dei semafori inizia con uno slash

int contatore=0; // variabile globale condivisa da tutti i thread
sem_t *mutex; // semaforo mutua esclusione

void * codice_thread(void * a) {
    pthread_t tid;
    int i;

    tid  = pthread_self();      // library tid
    printf("Sono il thread %lu del processo %i\n",*(unsigned long *)&tid,getpid());

    for(i=0;i<MAX;i++) {

        // sezione critica basta mettere una sem_wait prima di contatore++
        // e una sem_post dopo contatore++. Poiche' mutex è inizializzato
        // a 1 queta costruzione realizza una sezione critica dando
        // accesso a un thread alla volta.

        sem_wait(mutex); // P
        contatore++; // variabile globale condivisa ==> INTERFERENZE!!!
        sem_post(mutex); // V

    }
    pthread_exit(NULL);
}

int main() {
    pthread_t tid[2];
    int i,err;
    // create
    mutex = sem_open(SEM,O_CREAT,0600,1); // creiamo e inizializziamo a 1
    if (mutex == SEM_FAILED) {
        perror("Errore open");
        printf("%d %d\n",errno,EINVAL);
        exit(EXIT_FAILURE);
    }
    // crea i thread (ritorna 0 quando ha successo, vedere il man!)
    // - gli attributi sono quelli di default (il secondo parametro e' NULL)
    // - codice_thread e' il nome della funzione da eseguire
    // - non vegnono passati parametri (quarto parametro e' NULL)
    for (i=0;i<2;i++) {
        if ((err=pthread_create(&tid[i],NULL,codice_thread,NULL))) {
            printf("errore create [%i]\n",err);
            exit(EXIT_FAILURE); }
    }
    // attende i thread. Non si legge il valore di ritorno (secondo parametro NULL)
    for (i=0;i<2;i++) {
        if ((err=pthread_join(tid[i],NULL))) {
            printf("errore join [%i]\n",err);
            exit(EXIT_FAILURE); }
    }

    sem_close(mutex);
    sem_unlink(SEM);

    printf("I thread hanno terminato l'esecuzione correttamente. contatore=%d\n",
        contatore);
}

Esercizio 2: attesa tra 2 thread

Realizzare la sincronizzazione tra 2 thread vista a lezione in cui T2, prima di eseguire la porzione di codice < D > deve attendere che T1 abbia eseguito il codice < A >.

void * T1(void * j) {
  sleep(3);
  printf("Eseguito < A >\n");

  sleep(3);
  printf("Eseguito < B >\n");
}

void * T2(void * j) {
  printf("Eseguito < C >\n");

  printf("Eseguito < D >\n");
}

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>   /* For SYS_xxx definitions */
#include <semaphore.h>
// codice dei thread. Notare che e' una funzione che prende 
// un puntatore e ritorna un puntatore (a void)

sem_t sinc; // semaforo globale di sincronizzazione

void * T1(void * j) {
    sleep(3);
    printf("Eseguito < A >\n");

    // il semforo è rosso
    sem_post(&sinc); // OK a D di T2!

    sleep(3);
    printf("Eseguito < B >\n");
}

void * T2(void * j) {
    printf("Eseguito < C >\n");

    // il semaforo è rosso
    sem_wait(&sinc); // attende A di T1

    printf("Eseguito < D >\n");
}

int main() {
    pthread_t tid[2];
    int i,err;

    sem_init(&sinc,0,0); // semaforo rosso!

    if (err=pthread_create(&tid[0],NULL,T1,NULL)) {
            printf("errore create [%i]\n",err);
            exit(EXIT_FAILURE); }
    if (err=pthread_create(&tid[1],NULL,T2,NULL)) {
            printf("errore create [%i]\n",err);
            exit(EXIT_FAILURE); }

    // attende i thread

    for (i=0;i<2;i++) {
        if (err=pthread_join(tid[i],NULL)) {
            printf("errore join [%i]\n",err);
            exit(EXIT_FAILURE); }
    }

    sem_destroy(&sinc);
    printf("I thread hanno terminato l'esecuzione correttamente.\n");
}

Esercizio 3: Produttore-Consumatore

Implementare il Produttore-Consumatore con buffer circolare visto a lezione, utilizzando due semafori contatori più un mutex.

Partire dal seguente pseudo-codice:

semaphore piene=0, vuote=MAX, mutex=1;

Produttore {
  while(1) {
    < produce d >
    P(vuote); // richiede una cella vuota
    P(mutex); // entra in sezione critica
    buffer[inserisci] = d;
    inserisci = (inserisci+1) % MAX;
    V(mutex); // esce dalla sezione critica
    V(piene); // rilascia una cella piena
  }
}
 
Consumatore() {
  while( {
    P(piene); // richiede una cella piena
    P(mutex); // entra in sezione critica
    d = buffer[preleva];
    preleva = (preleva+1) % MAX:
    V(mutex); // esce dalla sezione critica
    V(vuote); // rilascia una cella vuota
    < consuma d >
  }
}

Testarlo in presenza di più produttori e più consumatori, verificando che la presenza del mutex è fondamentale per la coerenza dei dati. Inventarsi a tale scopo un test di qualche genere. Ad esempio si può tenere nelle celle vuote un valore speciale (-1) e testare se si sta leggendo una cella vuota o scrivendo in una piena. Ovviamente il consumatore deve scrivere nella cella il valore speciale, dopo che ha letto. Il test dovrebbe segnalare problemi di sincronizzazione nel caso non si utilizzino appropriatamente i semafori.

NOTA: l’output a video talvolta riduce le interferenze in quanto crea una alternanza molto forte nell’esecuzione dei thread. In tale caso, provare a fare la redirezione (tramite il simbolo >) su un file.

SOLUZIONE: non pubblico la soluzione di questo esercizio, provate da soli e mandate le vostre idee, commenti, tentativi, etc. su slack in modo che possiamo discuterne assieme!