LEZIONE del 3 Febbraio

Uno degli aspetti che hanno caratterizzato il C come ambiente di sviluppo ed implementazione dei sistemi, e che oggi si ritrova in molti altri linguaggi, è la facilità con cui è possibile strutturare i programmi in diversi file. Infatti, al contrario di alcuni schemi di programmazione che spingono a costruire programmi monolitici, in C un programma è tipicamente una collezione di funzioni.

Ad esse si aggiungono le necessarie definizioni di tipo e dichiarazioni di variabili le quali vengono distribuiti su un certo numero di file con estensioni .h e .c . I primi si dicono anche header file e contengono definizioni mentre i secondi sono costituiti da collezioni di funzioni. Si rammenta che fra queste deve comparire necessariamente il programma principale, che prende il nome di main.

Esempio di Programma MultiThread

Nell'esempio che segue si evince facilmente il caratteristico paradigma con con cui sono realizzati i programmi in C. La collezione di file descritta nel seguito comprende anche il file makefile il cui scopo è quello di contenere l'insieme di direttive ed informazioni supplementari necessarie alla generazione del codice eseguibile partendo dalla compilazione del codice sorgente contenuto nei singoli file.

L'insieme dei file che costituiscono il programma in questione è raccolto in un pacchetto tarball sorgente in formato di archivio compresso. Per ottenere da questo la directory contenente tutti i file sorgenti, è necessario eseguire nella bash shell il comando

tar xvf thread.tar.gz -z

dopodichè nella directory corrente comparirà una nuova directory con il nome thread contenente tutti i file in questione.

La collezione di file riportati nel seguito rappresenta un programma per la generazione di due thread che si sincronizzano per trasmettere dati attraverso un buffer secondo lo schema produttore consumatore.

Allo scopo di chiarificare il meglio possibile la sua realizzazione, la sua implementazione è ottenuta secondo lo schema della programmazione per oggetti, in cui alcuni file contengono definizioni di tipo e di funzioni per manipolare specifiche strutture dati.

Nel caso in esame sono stati realizzati le strutture dati buffer, mailbox e kernel che rappresentano, rispettivamente, la coda circolare in cui inserire effettivamente i dati, il canale usato dai processi per comunicare e le procedure per gestire sia i thread che la loro sincronizzazione.

BUFFER CIRCOLARE

I due file riportati di seguito implementano la coda circolare di messaggi utilizzata per immagazzinare i dati scambiati dai due processi. Il primo file, denominato buffer.h contiene le necessarie definizioni.

/*
 *  file buffer.h
 */


#define BUFFSIZE 5

typedef char Message ;

typedef struct {
        int lf ;
	int ld ;
	Message dd[BUFFSIZE] ;

} Buffer ;

extern void initbuffer(Buffer *Bu);
extern void putmsg(Buffer *Bu, Message Msg);
extern void getmsg(Buffer *Bu, Message *Msg);

Il secondo, con il nome di buffer.c contiene il codice specifico che implementa le funzioni che manipolano la coda circolare.

/*
 *  file buffer.c
 */

#include "buffer.h"


void initbuffer(Buffer *Bu)
{
  Bu->lf = 0;
  Bu->ld = 0;
}


void putmsg(Buffer *Bu, Message Msg)
{
  Bu->dd[Bu->ld] = Msg;
  Bu->ld++;
  if (Bu->ld == BUFFSIZE) Bu->ld = 0;
}


void getmsg(Buffer *Bu, Message *Msg)
{
  *Msg = Bu->dd[Bu->lf];
  Bu->lf++;
  if (Bu->lf == BUFFSIZE) Bu->lf = 0;
}

Si noti come il primo debba essere necessariamente incluso nel secondo per poter guidare il compilatore nella corretta generazione del codice oggetto delle procedure.

CANALE DI COMUNICAZIONE

La sincronizzazione dei messaggi inseriti e depositati nel buffer viene ottenuta da due semafori che hanno l'effetto, se necessario, di bloccare il processo consumatore quando il buffer è pieno oppure di sospendere il processo consumatore se il buffer risulta vuoto.

L'implementazione di tale meccanismo, che può vedersi anche come un canale di comunicazione fra processi, è ottenuta dai due file riportati di seguito. Il primo, denominato mailbox.h contiene le necessarie definizioni.

/*
 *  file mailbox.h
 */


#include "buffer.h"
#include "kernel.h"

typedef struct {
        semaphore notFull ;
	semaphore notEmpty ;
	Buffer Bu ;

} Channel ;

extern void newChannel(Channel *Chn);
extern void Send(Channel *Chn, Message Msg);
extern void Recv(Channel *Chn, Message *Msg);

Il secondo, avente il nome di mailbox.c contiene il codice specifico che implementa le funzioni che gestiscono il canale.

/*
 *  file mailbox.c
 */


#include "mailbox.h"


void newChannel(Channel *Chn)
{
  Qinit(&Chn->notFull, BUFFSIZE+1);
  Qinit(&Chn->notEmpty, 0);
  initbuffer(&Chn->Bu);
}


void Send(Channel *Chn, Message Msg)
{
  Qwait(&Chn->notFull);
  putmsg(&Chn->Bu, Msg);
  Qsignal(&Chn->notEmpty);
}


void Recv(Channel *Chn, Message *Msg)
{
  Qwait(&Chn->notEmpty);
  getmsg(&Chn->Bu, Msg);
  Qsignal(&Chn->notFull);
}

Si noti come il primo debba essere necessariamente incluso nel secondo per poter guidare il compilatore nella corretta generazione del codice oggetto delle procedure.

THREAD E SINCRONIZZAZIONE

La gestione dei thread e la relativa sincronizzazione è ottenuta con la seguente coppia di file. Il primo, kernel.h, contiene le necessarie definizioni le quali dipendono, in generale, dal tipo di piattaforma utilizzata. Ciò si ottiene scrivendo codice C per la compilazione condizionata, resa possibile dal C PreProcessor

/*
 *  file kernel.h
 */

/*-------------------------------------------------------------------
|                   definizioni per Unix Solaris                    |
-------------------------------------------------------------------*/
#ifdef Solaris
#include   <thread.h>
#include   <synch.h>

typedef    thread_t       process ;
typedef    sema_t         semaphore ;
#endif

/*-------------------------------------------------------------------
|                      definizioni per Linux                        |
-------------------------------------------------------------------*/
#ifdef Linux
#include   <pthread.h>
#include   <semaphore.h>

typedef    pthread_t      process ;
typedef    sem_t          semaphore ;
#endif


#define Procedure(X) void *(*X)(void *)

typedef    long int       integer ;
typedef    unsigned int   value ;


extern void NewThread(Procedure(proc), integer StackSize, process *TID);
extern int JoinThread(process TID, process Target);
extern void Thread(process *TID);

extern void Qinit(semaphore *s, value k);
extern void Qwait(semaphore *s);
extern void Qsignal(semaphore *s);
extern void Qvalue(semaphore *s, int *sval);

Il secondo, denominato kernel.c contiene il codice specifico che implementa sia la creazione e la gestione dei processi che le funzioni per la manipolazione dei semafori.

/*
 *  file kernel.c
 */

#include "kernel.h"

/*-------------------------------------------------------------------
|                creazione e gestione dei processi                  |
-------------------------------------------------------------------*/

void NewThread(Procedure(proc), integer StackSize, process *TID)
{
#ifdef Solaris
  if (thr_create(NULL, StackSize, proc, NULL, THR_NEW_LWP, TID))
    *TID = -1;
#endif
#ifdef Linux
  if (StackSize == 0) {
    if (pthread_create(TID, NULL, proc, NULL))
      *TID = -1; }
  else {
    perror("thread cannot be created\n") ;
    exit(2) ;}
#endif
};


int JoinThread(process TID, process Target)
{
#ifdef Solaris
   return thr_join(TID, &Target, NULL);
#endif
#ifdef Linux
   return pthread_join(TID, NULL);
#endif
};

void Thread(process *TID)
{
#ifdef Solaris
  *TID = thr_self();
#endif
#ifdef Linux
  *TID = pthread_self();
#endif
};

/*-------------------------------------------------------------------
|                        gestione dei semafori                      |
-------------------------------------------------------------------*/

void Qinit(semaphore *s, value k)
{
  /* initialize the semaphore */

#ifdef Solaris
  sema_init(s, k, USYNC_THREAD, 0) ;
#endif
#ifdef Linux
  sem_init(s, 0, k) ;
#endif
} ;

void Qwait(semaphore *s)
{
#ifdef Solaris
  sema_wait( s ) ;
#endif
#ifdef Linux
  sem_wait( s ) ;
#endif
} ;

void Qsignal(semaphore *s)
{
#ifdef Solaris
  sema_post( s ) ;
#endif
#ifdef Linux
  sem_post( s ) ;
#endif
} ;

void Qvalue(semaphore *s, int *sval)
{
#ifdef Solaris
w = sema_getvalue( s, sval ) ;
#endif
#ifdef Linux
w = sem_getvalue( s, sval ) ;
#endif
} ;

Si noti come il primo debba essere necessariamente incluso nel secondo per poter guidare il compilatore nella corretta generazione del codice oggetto delle procedure.

PROGRAMMA PRINCIPALE

La creazione e la gestione dei thread nel caso specifico è realizzata dal programma principale al quale si affiancano le due procedure attivate come thread. Il file in questione, che prende il nome di trace.c, è riportato di seguito e contiene istruzioni per la compilazione condizionata a seconda che si voglia avere o meno un traccia dettagliata della sua esecuzione.

/*
 *  file trace.c
 */

#include <stdio.h>
#include <unistd.h>
#include "mailbox.h"

void *Leggi();
void *Scrivi();

Channel Mbx;


main()
{
  process T0, T1, T2 ;

#ifdef DEBUG
  FILE *dbg;
  dbg = fopen("trace.dbg", "w");

  Thread( &T0 );
  fprintf(dbg, "Main thread = %d\n", T0);
#endif

  newChannel( &Mbx );  	/* inizializzazione del canale


      /* crea due processi "leggeri" */

  NewThread( Leggi, 0, &T1 );
  if (T1 == -1) fprintf(stderr, "Can't create thread 1\n"), exit(1);
#ifdef DEBUG
  fprintf(dbg, "Nuovo thread %d\n", T1);
#endif

  NewThread( Scrivi, 0, &T2 );
  if (T2 == -1) fprintf(stderr, "Can't create thread 2\n"), exit(1);
#ifdef DEBUG
  fprintf(dbg, "Nuovo thread %d\n", T2);
#endif

      /* ricongiungi i due flussi */

  if (JoinThread(T1, T2)) fprintf(stderr, "pthread_join error\n"), exit(2);
#ifdef DEBUG
  fprintf(dbg, "Thread %d terminated.\n", T1);
  fprintf(dbg, "Thread %d terminated.\n", T2);
#endif

      /* termina */

#ifdef DEBUG
  fprintf(dbg, "Process %d: exiting...\n", getpid());
#endif
  exit(0);
}


void *Leggi()
  {
    char ch;
    int cmd;
    int i;
    int w;
    int offset;
    process pid;
#ifdef DEBUG
    int NotFull;
    int NotEmpty;
    FILE *dbg;
    dbg = fopen("trace.dbg", "w");
#endif

    Thread(&pid);

    printf("programma di test per i thread\n");
    printf("enter an integer: ");
    w = scanf("%d\n", &cmd);
    if (cmd < 31) offset = 97;
    else if (cmd < 63) offset = 65;
    else if (cmd < 95) offset = 32;
    else offset = 48;

    for(i = 0; i < cmd; i = i + 1) {
#ifdef DEBUG
      Qvalue(&(Mbx.notFull), &NotFull);
      Qvalue(&(Mbx.notEmpty), &NotEmpty);
      fprintf(dbg, "\n   notFull = %d   notEmpty = %d\n", NotFull, NotEmpty);
#endif
      ch = offset + i;
      Send(&Mbx, ch);
    }
    Send(&Mbx, '!');
  }

void *Scrivi()
  {
#define MAXLINE 24
    char ch;
    integer again;
    process pid;
    int k;

    Thread(&pid);
    again = 1;
    k = 0;

    while(again) {
      Recv(&Mbx, &ch);
      if (ch == '!') again = 0 ;
      else {
        if (k == MAXLINE) {k = 0; printf("\n");}
        putchar(ch);
	k++;
      }
    }
    printf("\n\n");
  }

Makefile

Nell'ultimo file riportato sono contenute tutte le istruzioni necessarie per la compilazione dei singoli file sorgenti, in base alla piattaforma che si utilizza e se si intende avere la traccia esecutiva o meno.

#
# makefile per costruire il file eseguibile trace
#
# Seguono le definizioni di alcune macro
#
.SUFFIXES: .C $(SUFFIXES)

CC = gcc

CCLIBS_SunOS_5  = -lthread
CCLIBS_Linux    = -lpthread
CCLIBS          = $(CCLIBS_Linux)

OSFLAGS_SunOS_5 = -DSolaris -DDEBUG
OSFLAGS_Linux   = -DLinux -DDEBUG
CCFLAGS = -O2 -pipe $(OSFLAGS_Linux)

SRCS            = buffer.c kernel.c mailbox.c trace.c
OBJS            = $(SRCS:.c=.o)
DEFS            = $(SRCS:.c=.h)

TARGET          = trace

$(TARGET):	$(OBJS)
	$(CC) -o $@ $(OBJS) $(CCLIBS)

.c.o:
	$(CC) -c $(CCFLAGS) $<

clean:
	rm -f $(TARGET) $(OBJS) *core *bak *~

depend:
	makedepend $(DEFS) $(SRCS)

Il file di comandi è configurato per essere compilato in ambiente Linux ma, con una semplice variazione di due righe, può compilarsi anche sotto Solaris.