Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Clemente Galdi » 24.Le primitive di sincronizzazione in applicazioni multi-thread


Thread e sincronizzazione

Caratteristica principale della programmazione multi-thread è la condivisione della memoria da parte dei thread della stessa applicazione.
Questa caratteristica crea il rischio concreto che si creino situazioni di inconsistenza delle diverse strutture dati in memoria a causa delle modifiche concorrenti da parte di più thread.
Per evitare incongruenze sono necessari meccanismi di sincronizzazione.
La sincronizzazione NON è necessaria quando:

  • Tutti I thread accedono ad una struttura dati in sola lettura. Caso poco interessante visto che stiamo definendo l’accesso ad una costante.
  • Una struttura dati viene letta e modificata sempre dallo stesso thread. Visto che ogni thread “esegue una operazione per volta”, questi può, in ogni istante, leggere oppure modificare una struttura dati.

La sincronizzazione è necessaria quando almeno due thread accedono ad una struttura dati condivisa (variabili globali, statiche e dinamiche) e, tra questi, almeno un thread può modificarla.

Thread e sincronizzazione (segue)

Attenzione: TUTTE le operazioni possono essere NON atomiche. La stessa operazione può risultare atomica su una architettura e non atomica su un’altra. E.g. Si assuma l’esecuzione di “x++” sulla stessa variabile x in due thread della stessa applicazione.
Ogni incremento può diventare la seguente sequenza di istruzioni atomiche:

  • Carica la variabile x in un registro
  • Incrementa il registro
  • Memorizza il valore del registro in x.

Un possibile scheduling di due incrementi concorrenti potrebbe essere:

1. Carica la variabile x nel registro A
1. Incrementa il registro A
2. Carica la variabile x nel registro B
2. Incrementa il registro B
2. Memorizza il valore del registro B in x.
1. Memorizza il valore del registro A in x.

Al termine dei due incrementi, il valore della variabile x risulta incrementato di un unità invece che di due unità. Chiaramente la situazione può complicarsi notevolmente in applicazioni “reali” in cui diversi thread operano su strutture dati diverse, complesse ed interdipendenti.

Inconsistenza: un esempio

Esempio di inconsistenza causata dalla mancata sincronizzazione dei thread: Mostra codice.
Il programma inizializza una variabile globale di tipo myfoo a (1,1) (codice omesso) e crea 10 thread. Ogni thread incrementa le due componenti delle variabile condivisa. L'effetto atteso dovrebbe essere la visualizzazione di coppie di interi uguali (a=x,b=x).
La presenza di una attesa forzata nei thread è puramente didattico e tende a forzare l'interleaving tale per cui "l'incremento della componente b da parte di ogni thread viene effettuata solo dopo che tutti i thread hanno incrementato la componente a".
Si noti che, sebbene questo interleaving sia forzato, è sicuramente uno dei tanti possibili.

L'output: Mostra codice è sicuramente diverso da quello atteso.

In generale, la possibilità di interrompere un thread in qualsiasi momento rende possibile la visualizzazione di coppie (a=x, b≤x).

I mutex

Per eliminare le situazioni di inconsistenza, è necessario utilizzare primitive di sincronizzazione che consentano di “proteggere” I dati durante la loro modifica. Un primo esempio di primitive di sincronizzazione sono i mutex.
Un mutex è un semaforo binario ed è utilizzato per bloccare l’accesso ad una struttura dati.
Un thread:

  • “blocca” il (o “acquisisce il lock sul”) mutex prima di accedere alla struttura dati;
  • esegue l’accesso;
  • “sblocca” il mutex.

Un thread che tenti di “bloccare” un mutex già acquisito, viene posto in uno stato di attesa.
Quando il mutex viene sbloccato, tutti I thread in attesa su di esso diventano, di nuovo, eseguibili. Il primo tra essi ad essere schedulato, riacquisirà il mutex, forzando gli altri thread a ritornare in attesa.
Questo meccanismo di mutua esclusione funziona solo se l’applicazione è progettata in modo che tutti I thread che accedono ad una variabile condivisa acquisiscano esplicitamente il lock sul mutex prima dell’accesso. Difatti il sistema operativo:

  • Garantisce le proprietà di “atomicita” delle primitive di gestione dei mutex.
  • NON impedisce, per se, l’acceso a dati condivisi in mancanza di acquisizioni esplicite di lock su mutex.

I mutex (segue)

Un mutex è memorizzato in una variabile di tipo pthread_mutex_t
Un variabile di questo tipo deve essere allocata (staticamente o dinamicamente) e deve essere inizializzata.
Inizializzazione di un mutex:

  • se la variabile pthread_mutex_t è allocata staticamente, è sufficiente assegnare al mutex il valore della costante PTHREAD_MUTEX_INITIALIZER. L’esempio che segue, dichiara un mutex “c” staticamente e lo inizializza:
    • pthread_mutex_t c = PTHREAD_MUTEX_INITIALIZER
  • se la variabile pthread_mutex_t è allocata dinamicamente, è necessario utilizzare la funzione pthread_mutex_init, la cui firma è la seguente: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    • Il primo parametro di pthread_mutex_init è un puntatore alla variabile contenente il mutex ed il secondo parametro è un puntatore agli attributi del mutex (NULL per gli attributi standard).
    • Nel caso in cui il mutex sia allocato dinamicamente, è necessario invocare la funzione pthread_mutex_detroy prima di deallocare la variabile.

int pthread_mutex_destroy(pthread_mutex_t *mutex);

Utilizzo dei mutex

La libreria pthread fornisce le seguenti funzioni per l’utilizzo dei mutex:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

Tutte le funzioni prendono come parametro un puntatore ad un mutex. La funzione pthread_mutex_lock tenta di acquisire il mutex.

  • Se questi è libero, il mutex diventa bloccato e la funzione ritorna;
  • Se questi è bloccato, il thread viene sospeso.

La funzione pthread_mutex_unlock sblocca il mutex. Ogni thread può sbloccare solo I mutex su cui ha acquisito un lock. I.e., un thread non può sbloccare un mutex acquisito da un altro thread. I thread che, per qualche ragione, non possono essere sospesi durante l’attesa di un mutex, possono utilizzare la funzione pthread_mutex_trylock per acquisire il mutex.

  • Se questi è libero, il mutex diventa bloccato e la funzione ritorna;
  • Se questi è bloccato, la funzione ritorna immediatamente il codice d’errore EBUSY.

La differenza sostanziale tra lock e trylock è che nel caso di mutex bloccati, trylock non sospende il processo. La gestione dei mutex, in questi casi, è più complessa perché è necessario tentare periodicamente di riacquisire il lock sul mutex.

Inconsistenza: un esempio

Modifichiamo la funzione di incremento vista precedentemente, utilizzando I mutex.
Un primo tentativo (ERRATO) di correzione del codice: Mostra codice
Innanzitutto, come è ovvio che sia, I mutex devono essere condivisi tra tutti I thread che li utilizzeranno.
In figura viene dichiarato ed inizializzato un mutex "sem".
La funzione inc acquisisce il lock prima di modificare la componente "a" della struttura myfoo e rilascia il lock dopo aver terminato le modifiche.
Da un lato, questo codice corregge l'errore che consentiva la visualizzazione di coppie (a=x, b≤x).
Infatti, in questa versione, le componenti vengono sempre modificate entrambe.

L'output del programma: Mostra codice.

Inconsistenza: un esempio (segue)

Accesso concorrente a variabili condivise con sincronizzazione.
Il codice nell’esempio precedente è SBAGLIATO perché lascia fuori dalla sezione critica l’accesso in lettura alla variabile test contenuta all’interno della printf.
Difatti, modificando il codice come indicato in: Mostra codice, possiamo ottenere l'output seguente: Mostra codice.
Tutti I thread incrementano le compoenenti a e b di test fino a quando uno dei thread non riscontra una inconsistanza, I.e. finchè test.a e test.b sono diverse.
In questo caso, l'esecuzione della exit causa la terminazione di tutti I thread.
L'output mostra che, fuori dalla sezione critica delimitata da lock ed unlock sul mutex, è possibile trovarsi in situazioni di inconsistenza dei dati.
Per questa ragione, le primitive di sincronizzazione devono essere utilizzate per proteggere TUTTI gli accessi a variabili condivise (soggette a modifica).

In breve: Errore comune è NON proteggere gli accessi in lettura a variabili condivise.

Sincronizzazione e deadlock

La sincronizzazione può essere:

  • Per sezione critica
    • SOLO quando una struttura condivisa viene modificata in un unico punto nel codice (esempio precedente);
    • è sufficiente associare un mutex alla sezione critica.
  • Per “struttura”
    • Quando la struttura può essere modificata in più punti nel codice;
    • Utile se più strutture devono essere condivise contemporaneamente;
    • È necessario associare un mutex alla “struttura”

Sincronizzazione e deadlock (segue)

In generale un’applicazione definire più mutex per proteggere diverse strutture dati. Inoltre, in molti casi, è necessario acquisire più mutex prima di accedere a diverse strutture in modo consistente. Nel caso in cui sia necessario utilizzare più mutex, è possibile generare situazioni di deadlock.

  • Nota: un processo “da solo” può creare un deadlock eseguendo un lock su un mutex che ha già acquisito. Un errore di questo tipo blocca definitivamente il thread, visto che nessun altro thread può rilasciare il lock acquisito dal thread bloccato.

Un modo possibile per evitare il deadlock è l’acquisizione dei mutex sempre nello stesso “ordine”.

  • Non sempre possibile!
  • Può essere necessario utilizzare algoritmi specifici e pthread_mutex_trylock

Sincronizzazione per strutture

Di seguito sono illustrati esempi di sincronizzazione ottenuta includendo un mutex all’interno di una struttura, le cui istanze vengono allocate dinamicamente: Mostra codice;
Per includere un mutex all'interno di una struttura è sufficiente dichiarare una (o più) variabili di tipo pthread_mutex_t all'interno della stessa, come mostrato nella figura in alto.

Di seguito una funzione di allocazione/inizializzazione: Mostra codice.
La funzione alloca dinamicamente una variabile di tipo "myfoo" ed inizializza I campi della struttura.
Notiamo che l'inizializzazione (obbligatoria) del mutex all'interno della variabile deve essere effettuata, in questo caso, necessariamente utilizzando la funzione pthread_mutex_init.

Sincronizzazione per strutture (segue)

La funzione inc è una variante (corretta) delle funzioni viste in precedenza.
In un ciclo, acquisisce il mutex, modifica il contenuto della variabile test, lo visualizza e rilascia il mutex.

L’unica differenza è il riferimento al mutex che viene passato alle funzioni lock ed unlock. Infatti, nel seguente esempio: Mostra codice, il mutex è contenuto nella struttura puntata da test.

Il programma principale: Mostra codice.

  • Dichiara un puntatore a myfoo, senza allocarlo. L'allocazione viene effettuata dalla funzione init_struct;
  • Invoca init_struct che alloca lo spazio per la struttura ed inizializza I suoi campi;
  • Crea 10 thread;
  • Esegue la join sui 10 thread, per evitare che il master thread termini prima dei thread creati, provocando anche la loro terminazione;
  • Quando tutti hanno terminato la loro esecuzione, invoca la pthread_mutex_distroy prima di liberare lo spazio utilizzato dalla struttura.

Le condition variables

Le condition variable costituiscono una seconda primitiva di sincronizzazione. Consentono a thread diversi di attendere il verificarsi di “condizioni” arbitrarie.
Le condition variable sono protette da un mutex. Un thread deve acquisire il mutex prima di poter modificare/valutare il valore della condizione. Inoltre il mutex garantisce la mutua esclusione in sezione critica.
Analogamente ai mutex, una condition variable:

  • Viene memorizzata in una struttura che, nel caso delle condition variable, è di tipo pthread_cond_t;
  • Può essere dichiarata sia staticamente che dinamicamente.
  • La struttura va obbligatoriamente inizializzata
    • Se la struttura è dichiarata staticamente, l’inizializzazione va effettuata come segue:
      • pthread_cond_t c = PTHREAD_COND_INITIALIZER
    • se la struttura è allocata dinamicamente, l’inizializzazione deve seguire necessariamente (ovviamente) l’allocazione e viene effettuata come segue:
      • pthread_cont_t *c=malloc(sizeof(pthread_cond_t));
      • pthread_cond_init(c, NULL);
    • Per le condition variable allocate dinamicamente, è necessario invocare la funzione pthread_cond_destroy prima di deallocare lo spazio occupato dalla struttura.

Attesa di condition variables

La libreria pthread fornisce le seguenti funzioni per attendere il verificarsi di una condizione:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec timeout);

La funzione pthread_cond_wait riceve com parametri la condition variable ed un mutex.
Il mutex viene utilizzato per proteggere la condizione. Quando viene invocata, il mutex deve essere stato acquisito dal thread.
Se a condizione è falsa, la funzione aggiunge il thread corrente all’elenco dei thread in attesa di quella condizione, sospende il thread e rilascia il mutex.
La funzione pthread_cond_wait ritorna quando la condizione è vera. Inoltre, quando la funzione ritorna, il mutex è nuovamente bloccato.

La funzione pthread_cond_timedwait, consente di specificare un timeout attraverso la struttura passata come terzo parametro. Il comportamento è lo stesso della cond_wait, con la differenza che, trascorso il timeout, la funzione ritorna con un codice d’errore ETIMEDOUT.

In ogni casi, quando una delle funzioni di wait ritorna, il thread deve verificare nuovamente la condizione dato che un altro thread può aver modificato nuovamente lo stato della condizione.
Per questa ragione, le funzioni di wait sono sempre inserite all’interno di un ciclo per la verifica della condizione di attesa.

Notifica di condition variables

La libreria pthread fornisce le seguenti funzioni per notificare la modifica dello stato di una condizione:

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

La funzione pthread_cond_signal risveglia esattamente un thread in attesa sulla condition variable cond.
La funzione pthread_cond_broadcast risveglia tutti I thread in attesa sulla condition variable cond.

Le condition variable possono essere utilizzate come punto di “rendevouz” in applicazioni multi-thread.
Si pensi ad esempio ad una applicazione multi-thread produttore/consumatore con buffer infinito, in cui vi sono più thread produttori e più thread consumatori.
Visto che il buffer è infinito, i thread produttori possono sempre “produrre” elementi da aggiungere al buffer. Devono solo utilizzare un mutex per garantire la mutua esclusione durante la fase di modifica del buffer.
Al contrario, tutti i consumatori devono attendere la presenza di elementi nel buffer prima di poterli consumare.

Condition variables

Le condition variable possono essere utilizzate come punto di “rendevouz” in applicazioni multi-thread.
Si pensi ad esempio ad una applicazione multi-thread produttore/consumatore con buffer infinito, in cui vi sono più thread produttori e più thread consumatori.
Visto che il buffer è infinito, i thread produttori possono sempre “produrre” elementi da aggiungere al buffer. Devono comunque utilizzare un mutex (od un’altra primitiva di sincronizzazione) per garantire la mutua esclusione durante la fase di modifica del buffer.
Al contrario, tutti i consumatori devono attendere la presenza di elementi nel buffer prima di poterli consumare.
Il punto di rendevouz per I consumatori, in questo caso, è l’ingresso della sezione critica. Quando non vi sono elementi da consumare, i consumatori possono “incontrarsi” all’ingresso della seione critica attendendo su una condition variable.
Quando I produttori generano elementi, uno dei (o tutti i) consumatori in attesa su una condition variable possono essere “potenzialmente” risvegliati. D’altro canto, un solo thread per volta viene effettivamente risvegliato, visto che un solo thread per volta riesce ad acquisire il mutex per uscire dalla funzione wait.
L’unico consumatore attivo controlla la condizione, processa il buffer e rilascia il mutex, consentendo il risveglio “effettivo” di un altro consumatore (o l’ingresso in sezione critica di un altro produttore), e così, via.
Se, ad un certo punto, il buffer è vuoto, tutti i consumatori “potenzialmente risvegliati”, acquisiscono il mutex, riverificano la condizione e ritornano in attesa sulla condition variable.

Utilizzo condition variable

Struttura di un thread che attende il verificarsi di una condizione: Mostra codice.

Acquisisce il lock utilizzato per proteggere la condition variable;

Finché la condizione è falsa, attende sulla condition variable, rilasciando il lock.

Quando la cond_wait ritorna, Il thread verifica di nuovo la condizione e, nel caso in cui sia vera, esegue la sezione critica. Di nuovo, Si noti che la mutua esclusione è garantita dal fatto che, il ritorno della cond_wait garantisce l'acquisizione del mutex.

Al termine della sezione critica, viene rilasciato il mutex per consentire, ad altri thread di proseguire.
Schema di un produttore: Mostra codice.
In questo caso, è sufficiente acquisire il lock, prima di entrare in sezione critica, eseguire la notifica ai thread in attesa e rilasciare il mutex.
L'asimmetria tra produttore e consumatore è strettamente legata alle seguenti osservazioni:

I produttori possono produrre sempre. I.e., NON devono attendere mai, a meno della mutua esclusione.
I consumatori devono attendere, oltre che per la mutua esclusione, anche la presenza di elementi a consumare.

Produttore/consumatore con buffer infinito

Esempio di utilizzo delle condition variable. Il master thread crea un thread produttore: Mostra codice, e tre thread consumatori: Mostra codice
La funzione eseguita dal thread consumatore, decrementa una variabile intera solo se questa è diversa da zero.
All'interno di un ciclo infinito:

  • Acquisisce il mutex sem;
  • Finchè a=0, si pone in attesa sulla condition variable cond;
  • Quando la wait ritorna, se a è diversa da zero, ne visualizza il valore, lo decrementa (lo "consuma") e rilascia il semaforo;
  • Infine, tenta di riacquisire il semaforo e, se a è di nuovo pari a zero si rimette in attesa.

Il produttore, incrementa la variabile a, al più 100 volte. Ad ogni iterazione:

  • acquisisce il mutex;
  • incrementa a ("produce" un elemento) e visualizza il suo valore;
  • esegue la signal sulla condition variable;
  • rilascia il semaforo.

Anche in questo caso, la presenza di una attesa (nanosleep) è necessaria ai soli fini didattici.

Il comportamento con signal

Thread consumatore: Mostra codice
Consideriamo l'effetto delle funzioni signal e broadcast sul comportamento dei consumatori. Di seguito riportiamo le prime righe dell'output del programma: Mostra codice
In questa versione, il produttore utilizza la signal.
All'atto della creazione, I tre thread consumatori trovano la condizione falsa e si mettono in attesa sulla condition variable.
A questo punto viene creato il thread produttore. Quindi:

  • Il produttore produce un elemento e si mette in attesa
  • La signal risveglia un solo consumatore che consuma l'elemento, ricontrolla la condizione trovandola falsa e ritorna in attesa, e così via.

Notiamo che, anche in questo esempio, la sequenza di scheduling è fortemente condizionata da una attesa forzata del produttore di 200ms.

Il comportamento con broadcast

Thread consumatore: Mostra codice

Nella seconda versione, il produttore utilizza la funzione pthread_cond_broadcast.
Di seguito le prime righe dell'output del programma: Mostra codice
A differenza della versione precedente, in questo caso dopo ogni elemento prodotto, vengono risvegliati tutti i thread consumatori. Tra questi, il primo consuma l'unico elemento prodotto, mentre gli altri ritrovano la condizione falsa e ritornano in un stato di attesa.

Produttore/consumatore con buffer limitato

Thread consumatore: Mostra codice.

Thread produttore: Mostra codice.

Nel caso con buffer limitato, ad esempio, 0<a<11 sorge il problema di verificare una condizione anche per il produttore. È possibile pensare ad una soluzione ibrida tra I due schemi visti in precedenza.

Sia il produttore che il consumatore: Verificano una condizione e, se è falsa, si mettono in attesa su una condition variable: Il consumatore verifica se a=0, mentre il produttore verifica se (a+1)>10.

È fondamentale che produttori e consumatori utilizzino lo stesso mutex per garantire la mutua esclusione dalla sezione critica. In questa soluzione, però, sia il produttore che il consumatore devono risvegliare "altri thread".

Nell'esempio si è scelto di utilizzare due condition variable, una per I produttori ed una per I consumatori. è possibile, però, pensare a soluzioni con un'unica condition variable.

Anche la "politica" utilizzata per risvegliare I thread dipende dal contesto. In questo caso con due condition variable, un consumatore risveglia tutti I produttori in attesa mentre un produttore risveglia un solo consumatore in attesa.

Thread e segnali

La gestione dei segnali all’interno di applicazioni multi-thread deve tenere conto delle seguenti peculiarità:

L’elenco delle azioni è condivisa tra tutti I thread. Se un thread modifica la azione associata ad un segnale, la modifica è immediatamente visibile a tutti I thread della stessa applicazione.
Un segnale viene consegnato ad un solo thread dell’applicazione. Tipicamente, I segnali legati a fault hardware vengono consegnati al thread che ha generato il fault. Gli altri segnali vengono consegnati in modo “arbitrario”.

  • Se il segnale non viene intercettato e se l’azione di default associata al segnale consiste nel terminare il processo, tutti i thread vengono terminati

Ogni thread possiede una propria maschera dei segnali bloccati. Diversi thread possono bloccare segnali diversi.
Il comportamento della sigprocmask è indefinito nelle applicazioni multithread. Per manipolare la maschera dei segnali, I thread devono utilizzare la seguente:

int pthread_sigmask(int how, const sigset_t *set, sigset_t oset);

La funzione pthread_sigmask riceve gli stessi parametri della system call sigprocmask.

Thread e segnali: attesa ed invio

Un thread può attendre per uno o più segnali utilizzando la funzione:
int pthread_sigwait(sigset_t *set, int *signop)

Dove set definisce l’insieme dei segnali per cui si è in attesa. Quando a funzione ritorna, signop punta ad un intero che indica il numero del segnale ricevuto.

  • è possibile inviare un segnale “al processo” utilizzando la system call kill. In questo caso, il segnale sarà consegnato ad un thread scelto in modo “arbitrario”.

La libreria pthread consente, però, di inviare anche un segnale ad un thread specifico utilizzando la funzione:

int pthread_kill(pthread_t tid, int signo);

Tid identifica il thread a cui inviare il segnale definito dal parametro signo.

  • se è impostato un handler, viene eseguito nel thread tid;
  • se non è impostato un handler, e se il comportamento di default è di terminare il processo, vengono comunque terminati tutti i thread.

I thread e segnali: un esempio

Il seguente programma: Mostra codice riporta un esempio di utilizzo dei segnali nei thread.
Viene installato l'handler per il segnale SIGUSR1.
L'handler usr1 visualizza un messaggio contenente il tid del thread corrente.
A questo punto vengono creati tre thread e, viene inviato SIGUSR1 ad ogni thread. L'output visualizza I tre messaggi distinti: Mostra codice
A questo punto, utilizzando la funzione pthread_sigmask, viene modificata la maschera del master thread aggiungendo il segnale SIGUSR1.
Per effetto di questa modifica, il master thread non riceve più il segnale SIGUSR1.
Viene quindi inviato il segnale SIGUSR1 dieci volte utilizzando la system call kill. Da notare che:

  • Il master thread on riceve mai il segnale (il tid delle ultime 10 righe corrisponde al tid del thread 1).
  • Come è possibile intuire, la selezione del thread a cui consegnare il segnale NON è completamente casuale.

Thread e fork

Un thread può invocare la system call fork e creare un nuovo processo figlio.
Indipendentemente dal numero di thread nell’applicazione originaria, il nuovo processo consiste di un solo thread, copia del thread che ha invocato la fork.
Il processo figlio, però, eredita anche lo stato di tutte le primitive di sincronizzazione nel processo padre.
Il problema sta nel fatto che:

  • In generale, il figlio non ha modo, di sapere quale primitiva sia attualmente bloccata.
    • Per I mutex è possibile utilizzare trylock. In generale, però questa possibilità non esiste per tutte le primitive.
  • Se esegue un lock su una primitiva bloccata, è condannato al deadlock perché, a differenza del processo padre, non esiste nel figlio il thread che ha la possibilità di eseguire l’unlock di quella primitiva.

Questo problema può essere risolto se il processo appena creato invoca la system call exec. In questo caso, tutte le primitive di sincronizzazione, ed il loro stato, vengono cancellate dal contesto del nuovo programma da eseguire.
Chiaramente, in molti casi, non è possibile eseguire la exec.

Thread e fork (segue)

L’idea per creare un processo figlio in cui tutte le primitive sono sbloccate si basa sulla seguente proprietà: Se è il processo padre ad eseguire il lock di tutte le primitive prima della fork, allora padre e figlio potranno entrambi eseguire l’unlock dopo la fork.
Per ripulire lo stato dei lock, è possibile installare i lock handler utilizzando la seguente funzione:
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
La atfork riceve tre puntatori a funzione. Tutte le funzioni vengono eseguite dopo l’inizio della esecuzione della fork:

  • prepare: eseguita prima che la fork crei il figlio. Esegue il lock di TUTTE le primitive utilizzate nel thread.
  • parent: eseguita nel contesto del padre (dopo la creazione del figlio). Esegue l’unlock di TUTTE le primitive utilizzate nel thread.
  • child: eseguita nel contesto del figlio. Esegue l’unlock di TUTTE le primitive utilizzate nel thread.

Si noti che, la presenza di due distinte funzioni di unlock non corrisponde ad eseguire due volte l’unlock sulla stessa primitiva. L’unlock viene eseguito sulla stessa primitiva ma in due processi diversi, una volta nel padre ed una volta nel figlio.
Questa strategia consente di avere tutte le primitive di sincronizzazione sbloccate sia nel padre che nel figlio.

I materiali di supporto della lezione

W.R. Stevens – S.A. Rago, Advanced Programming in the Unix Environment, Capitolo 11 (11.6), Capitolo 11 (12.8-9).

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion