Disclaimer :
La versione originale di questo articolo è stata pubblicata da IBM
developerWorks ed è di proprietà di Westtech Information Services. Questo
documento è una versione aggiornata dell'articolo originale, e contiene
numerosi miglioramenti apportati dal Gentoo Linux Documentation team.
Questo documento non è mantenuto attivamente.
|
Spiegazioni sui thread POSIX, parte 3
1.
Migliorare l'efficenza con le variabili di condizione
Le variabili di condizione spiegate
Ho finito il mio articolo
precedente descrivendo un particolare dilemma su come faccia un thread a
gestire una situazione in cui sta aspettando che una determinata condizione
diventi vera. Potrebbe ripetutamente bloccare /sbloccare un mutex, controllando
ogni volta per un certo valore una struttura dati condivisa. Ma questa è una
perdita di tempo e di risorse e questa forma di busy polling è estremamente
inefficiente. Il miglior modo per farlo è usare la chiamata pthread_cond_wait()
per attendere che una determinata condizione diventi vera.
E' importante capire che cosa pthread_cond_wait() faccia -- è il cuore del
sistema di segnalazione dei thread di POSIX ed è anche la parte più difficile
da capire.
Per prima cosa consideriamo una scenario in cui un thread ha bloccato un
mutex per leggere una lista linkata e la lista è vuota. Questo particolare
thread non può fare nulla -- è scritto per togliere un nodo dalla lista e
non ce ne sono disponibili. Così ecco cosa fa.
Mentre continua a tenere il mutex bloccato, il nostro thread chiama
pthread_cond_wait(&mycond,&mymutex) la chiamata a pthread_cond_wait() è
abbastanza complessa così che affrontiamo ciascuna operazione un passo
alla volta.
La prima cosa che p_thread _cond_wait() fa è bloccare il mutex mymutex (così
che gli altri thread possono modificare la lista linkata) e contemporaneamente
aspetta la condizione mycond (così che pthread_cond_wait() si sveglia quando
riceve un segnale da un altro thread). Ora che il mutex è sbloccato, altri
thread possono accedere e modificare la lista linkata, possibilmente aggiungendo
altri oggetti.
A questo punto la chiamata pthread_cond_wait() non è ancora ritornata.
Lo sbloccaggio del mutex avviene immediatamente, ma aspettare per la condizione
mycond è normalmente un'operazione che blocca, ciò significa che il nostro
thread va in sleep, senza consumare alcun ciclo di CPU fine al momento del
risveglio. Questo è esattamente quello che vogliano che succeda. Il nostro
thread è in sleep, aspettando che una determinata condizione diventi vera,
senza fare nessun tipo di "busy polling" che sprecherebbe tempo di CPU. Dal
punto di vista del nostro thread, sta semplicemente aspettando che ritorni la
chiamata pthread_cond_wait().
Ora, per continuare con la spiegazione, diciamo che un altro thread
(chiamiamolo "thread 2") blocchi mymutex e aggiunga un oggetto alla nostra
lista linkata. Immediatamente dopo aver sbloccato il mutex, il thread 2 chiama
la funzione pthread_cond_broadcast(&mycond). Facendo questo, il thread 2 fa
svegliare immediatamente tutti quei thread che aspettavano la variabile di
condizione mycond. Questo significa che il nostro primo thread (che è nel mezzo
di una chiamata pthread_cond_wait()) adesso si sveglia.
Ora diamo un'occhiata a cosa succede al nostro primo thread. Dopo che il thread
2 ha chiamato pthread_cond_broadcast(&mymutex) potreste pensare che la
pthread_cond_wait() del thread 1 ritorni immediatamente. Non è così! Invece,
pthread_cond_wait() eseguirà un'ultima operazione: ribloccare mymutex. Una
volta che pthread_cond_wait() ha il blocco, allora ritornerà e permetterà a
thread 1 di continuare l'esecuzione. A quel punto, può immediatamente
controllare la lista per qualsiasi cambiamento degno di nota.
Fermati e riguarda!
Codice 1.1: queue.h |
/* queue.h
*/
typedef struct node {
struct node *next;
} node;
typedef struct queue {
node *head, *tail;
} queue;
void queue_init(queue *myroot);
void queue_put(queue *myroot, node *mynode);
node *queue_get(queue *myroot);
|
Codice 1.2: queue.c |
/* queue.c
*/
#include <stdio.h>
#include "queue.h"
void queue_init(queue *myroot) {
myroot->head=NULL;
myroot->tail=NULL;
}
void queue_put(queue *myroot,node *mynode) {
mynode->next=NULL;
if (myroot->tail!=NULL)
myroot->tail->next=mynode;
myroot->tail=mynode;
if (myroot->head==NULL)
myroot->head=mynode;
}
node *queue_get(queue *myroot) {
//get from root
node *mynode;
mynode=myroot->head;
if (myroot->head!=NULL)
myroot->head=myroot->head->next;
return mynode;
}
|
Codice 1.3: control.h |
#include <pthread.h>
typedef struct data_control {
pthread_mutex_t mutex;
pthread_cond_t cond;
int active;
} data_control;
|
Codice 1.4: control.c |
/* control.c
*/
#include "control.h"
int control_init(data_control *mycontrol) {
int mystatus;
if (pthread_mutex_init(&(mycontrol->mutex),NULL))
return 1;
if (pthread_cond_init(&(mycontrol->cond),NULL))
return 1;
mycontrol->active=0;
return 0;
}
int control_destroy(data_control *mycontrol) {
int mystatus;
if (pthread_cond_destroy(&(mycontrol->cond)))
return 1;
if (pthread_mutex_destroy(&(mycontrol->cond)))
return 1;
mycontrol->active=0;
return 0;
}
int control_activate(data_control *mycontrol) {
int mystatus;
if (pthread_mutex_lock(&(mycontrol->mutex)))
return 0;
mycontrol->active=1;
pthread_mutex_unlock(&(mycontrol->mutex));
pthread_cond_broadcast(&(mycontrol->cond));
return 1;
}
int control_deactivate(data_control *mycontrol) {
int mystatus;
if (pthread_mutex_lock(&(mycontrol->mutex)))
return 0;
mycontrol->active=0;
pthread_mutex_unlock(&(mycontrol->mutex));
pthread_cond_broadcast(&(mycontrol->cond));
return 1;
}
|
Debug time
Un altro file misto prima di arrivare a quello grosso. Ecco dbug.h:
Codice 1.5: dbug.h |
#define dabort() \
{ printf("Aborting at line %d in source file %s\n",__LINE__,__FILE__);
abort(); }
|
Usiamo questo codice per gestire errori irrecuperabili nel nostro codice work
crew.
Il codice di work crew
Parlando del codice di work crew code, eccolo:
Codice 1.6: workcrew.c> |
#include <stdio.h>
#include <stdlib.h>
#include "control.h"
#include "queue.h"
#include "dbug.h"
/* */
struct work_queue {
data_control control;
queue work;
} wq;
/* */
typedef struct work_node {
struct node *next;
int jobnum;
} wnode;
/* */
struct cleanup_queue {
data_control control;
queue cleanup;
} cq;
/* */
typedef struct cleanup_node {
struct node *next;
int threadnum;
pthread_t tid;
} cnode;
void *threadfunc(void *myarg) {
wnode *mywork;
cnode *mynode;
mynode=(cnode *) myarg;
pthread_mutex_lock(&wq.control.mutex);
while (wq.control.active) {
while (wq.work.head==NULL && wq.control.active) {
pthread_cond_wait(&wq.control.cond, &wq.control.mutex);
}
if (!wq.control.active)
break;
//we got something!
mywork=(wnode *) queue_get(&wq.work);
pthread_mutex_unlock(&wq.control.mutex);
//perform processing...
printf("Thread number %d processing job
%d\n",mynode->threadnum,mywork->jobnum);
free(mywork);
pthread_mutex_lock(&wq.control.mutex);
}
pthread_mutex_unlock(&wq.control.mutex);
pthread_mutex_lock(&cq.control.mutex);
queue_put(&cq.cleanup,(node *) mynode);
pthread_mutex_unlock(&cq.control.mutex);
pthread_cond_signal(&cq.control.cond);
printf("thread %d shutting down...\n",mynode->threadnum);
return NULL;
}
#define NUM_WORKERS 4
int numthreads;
void join_threads(void) {
cnode *curnode;
printf("joining threads...\n");
while (numthreads) {
pthread_mutex_lock(&cq.control.mutex);
/* */
while (cq.cleanup.head==NULL) {
pthread_cond_wait(&cq.control.cond,&cq.control.mutex);
}
/* */
curnode = (cnode *) queue_get(&cq.cleanup);
pthread_mutex_unlock(&cq.control.mutex);
pthread_join(curnode->tid,NULL);
printf("joined with thread %d\n",curnode->threadnum);
free(curnode);
numthreads--;
}
}
int create_threads(void) {
int x;
cnode *curnode;
for (x=0; x<NUM_WORKERS; x++) {
curnode=malloc(sizeof(cnode));
if (!curnode)
return 1;
curnode->threadnum=x;
if (pthread_create(&curnode->tid, NULL, threadfunc, (void *) curnode))
return 1;
printf("created thread %d\n",x);
numthreads++;
}
return 0;
}
void initialize_structs(void) {
numthreads=0;
if (control_init(&wq.control))
dabort();
queue_init(&wq.work);
if (control_init(&cq.control)) {
control_destroy(&wq.control);
dabort();
}
queue_init(&wq.work);
control_activate(&wq.control);
}
void cleanup_structs(void) {
control_destroy(&cq.control);
control_destroy(&wq.control);
}
int main(void) {
int x;
wnode *mywork;
initialize_structs();
/* CREATION */
if (create_threads()) {
printf("Error starting threads... cleaning up.\n");
join_threads();
dabort();
}
pthread_mutex_lock(&wq.control.mutex);
for (x=0; x<16000; x++) {
mywork=malloc(sizeof(wnode));
if (!mywork) {
printf("ouch! can't malloc!\n");
break;
}
mywork->jobnum=x;
queue_put(&wq.work,(node *) mywork);
}
pthread_mutex_unlock(&wq.control.mutex);
pthread_cond_broadcast(&wq.control.cond);
printf("sleeping...\n");
sleep(2);
printf("deactivating work queue...\n");
control_deactivate(&wq.control);
/* CLEANUP */
join_threads();
cleanup_structs();
}
|
Attraverso il codice
Adesso è ora di fare una veloce passeggiata attraverso il codice. La prima
struttura definita è chiamata "wq", e contiene un data_control e una queue
header. La struttura data_control viene usata per arbitrare l'accesso
all'intera coda, inclusi i nodi. Il nostro prossimo lavoro è definire
i reali nodi di lavoro. Per mantenere il codice snello in maniera da
farlo entrare in questo articolo tutto quello che c'è qui è un job number.
Successivamente creiamo una coda per cleanup. I commenti mostrano come ciò
funzioni. OK, per ora saltiamo le chiamate a threadfunc(), join_threads(),
create_threads() e initialize_structs(), e saltiamo alla main(). La prima
cosa che facciamo è inizializzare le nostre strutture -- questo include
l'inizializzare la nostra data_controls e le code, come anche attivare la
nostra coda di lavoro.
Cleanup special
Ora è il momento di inizializzare i nostri thread. Se si guarda alla chiamata
alla nostra create_threads(), tutto sembra piuttosto normale...eccetto una cosa.
Si noti che noi allochiamo un nodo cleanup, inizializziamo il suo numero di
thread e componenti TID. Passiamo inoltre un nodo cleanup a ciascun nuovo worker
thread come argomento iniziale.perché lo facciamo?
Perché quando un worker thread esce, attaccherà il suo nodo cleanup alla coda
cleanup e terminerà. In seguito il nostro thread principale noterà questa
aggiunta alla coda cleanup (grazie all'uso di una variabile di condizione) e
dequeue il nodo. Siccome la TID (id del thread) è salvata nel nodo cleanup, il
nostro thread principale saprà esattamente quale thread terminare. Allora il
nostro thread principale chiamerà pthread_join(tid), e con il worker si
attaccherà al thread appropriato. Se non facessimo questo tipo di controllo, il
nostro thread principale si dovrebbe attaccare ai worker thread in modo
arbitrario. Presumibilmente nell'ordine in cui sono stati creati. Siccome i
thread non devono necessariamente terminare in quest'ordine, il nostro thread
principale potrebbe stare aspettando di unirsi con un thread mentre avrebbe
potuto unirsi con altri dieci. Riuscite a vedere come questa scelta di
progetto possa realmente velocizzare il nostro codice di spegnimento
specialmente se si usano centinaia di worker thread?
Creare lavoro
Ora che abbiamo fatto partire i nostri worker thread (e che stanno facendo
andare le loro threadfunc(), di cui parleremo tra poco), il nostro thread
principale incomincia ad inserire oggetti all'interno della coda di work. Per
prima cosa, blocca il controllo mutex di wq, e poi alloca 16000 pacchetti work
inserendoli ad uno ad uno all'interno della coda. Dopo di che,
pthread_cond_broadcast() è chiamata così che, qualsiasi thread dormiente viene
svegliato è puo fare il lavoro. Allora il nostro thread principale dorme per
due secondi, dopo di che disattiva la work queue dicendo ai worker thread di
terminare. Quindi il nostro thread principale chiama le funzioni join_threads()
per pulire tutti i worker thread.
threadfunc()
E' ora di guardare threadfunc(), il codice che ciascun work thread esegue.
Quando un worker thread inizia, immediatamente blocca il mutex della work queue,
prende un node work (se disponibile) e lo processa. Se non c'è un work
disponibile pthread_cond_wait() viene chiamato. Noterete che è chiamato
in un ciclo while() molto stretto e questo è molto importante. Quando ci
si sveglia da una chiamata pthread_cond_wait(), non si dovrebbe mai
presupporre che la nostra condizione sia assolutamente vera, probabilmente lo
sarà ma potrebbe anche non esserlo. Il ciclo while() forza pthread_cond_wait()
ad essere richiamato se accadesse che il thread venisse erroneamente svegliato
e la lista fosse vuota.
Se c'è un work node, semplicemente stampiamo il suo numero di job, lo liberiamo
e usciamo. Nella realtà il codice farebbe qualche cosa di più sostanziale. Alla
fine del ciclo while(), blocchiamo il mutex così che possiamo controllare la
variabile attiva come anche controllare nuovi work node all'inizio del ciclo. Se
si segue il codice si troverà che il wq.control.active è 0, il ciclo while()
sarà terminato e il codice di cleanup alla fine di threadfunc() ricomincerà.
La parte del worker thread è abbastanza interessante. Primo sblocca la
work_queue, poiché se il mutex è bloccato phread_cond_wait() ritorna. Dopo di
che prende un lock sulla code cleanup, aggiunge il nostro cleanup node (che
contiene la nostra TID, che il thread principale userà per la sua chiamata a
pthread_join(), e dopo sbloccherà la cleanup queue. Dopo di che segnala a cq
waiters (pthread_cond_signal(&cq.control.cond)) così che il thread
principale sa che c'è un nuovo nodo da processare. Non usiamo
pthread_cond_broadcast() perché non è necessario -- solamente un thread (il
thread principale) sta aspettando nuove entry nella coda di cleanup. Il nostro
worker thread stampa un messaggio di spegnimento e poi termina aspettando di
essere pthread_joined() dal thread principale quando chiama join_threads().
join_threads()
Se volete vedere un semplice esempio di come le variabili di condizione
dovrebbero essere usate, data un'occhiata alla funzione join_threads(). Mentre
abbiamo ancora worker thread in esistenza, join_threads() cicla,
aspettando nuovi nodi cleanup nella nostra coda cleanup. Se c'è un nuovo
nodo, si dequeue il nodo, sblocca la cleanup queue (così che altri nodi
di cleanup possano essere aggiungi dai nostri worker thread), si unisce con
il nuovo thread (usando la TID memorizzata nel nodo cleanup), libera il nodo
cleanup, decrementa il numero di thread "li fuori" e continua.
Riassumendo
Siamo arrivati alla fine della serie "Spiegazione sui thread POSIX, parte
tre", e spero che ora siate pronti ad aggiungere codice multithreaded alle
vostre applicazioni. Per maggiori informazioni vogliate guardare la sezione
Resources, che contiene anche una tarball di tutti
i sorgenti usati in questo articolo. Alla prossima serie!
2.
Resources
-
E' disponibile un tarball
dei sorgenti usati in questo articolo.
-
Leggete gli articoli di Daniel, Spiegazioni sui thread POSIX Parte 1 e Parte
2.
-
Date sempre un'occhiata all'amichevole pagina del manuale LINUX di pthread
(man -k pthread).
-
Per una terapia d'urto raccomando questo libro:
Programming with POSIX Threads, di David R. Butenhof (Addison-Wesley,
1997). Questo è presumibilmente il miglior libro sui thread POSIX
disponibile.
-
I thread POSIX sono anche affrontati in questo libro:
UNIX Network Programming - Networking APIs: Sockets and XTI, di W.
Richard Stevens (Prentice Hall, 1997). Questo è un classico libro, ma non
copre i thread così in dettaglio come invece Programming with POSIX Threads
fa.
-
Guardate la documentazione su: Linux
threads, di Sean Walton, KB7rfa.
-
Consultate un tutorial
sui thread POSIX di Mark Hays, Università dell'Arizona.
-
In An Introduction to
Pthreads-Tcl, guardate i cambiamenti a Tcl che gli permettono di
essere usato con i thread POSIX.
-
Un'altro tutorial, Getting
Started with POSIX Threads, di Tom Wagner e Don Towsley del
dipartimento di Computer Science presso l'Università del
Massachusetts,Amherst.
-
FSU PThreads è
una libreria C che implementa i thread POSIX per SunOS 4.1.x,Solaris 2.x,
SCO UNIX, FreeBSD, Linux, e DOS.
-
Fate riferimento all'home page per thread POSIX e DCE
per Linux.
-
Guardate The
LinuxThreads library.
-
Proolix è
un semplice sistema operativo POSIX-compliant per i8086+ in continuo
sviluppo.
|
|
Aggiornato il 9 ottobre 2005 |
Oggetto:
In questo articolo, l'ultimo di una serie di tre sui thread POSIX, Daniel dà
una buona idea su come usare le variabili di condizione. Le variabili di
condizione sono strutture di thread di POSIX che vi permettono di "risvegliare"
i thread al verificarsi di certe condizioni. Potete pensare a loro come di una
forma di signalling thread sicura. Daniel riempie l'articolo usando tutto
quello che avete imparato fino ad adesso per sviluppare applicazioni work crew
multi-thread.
|
Daniel Robbins
Autore
Massimo Zanetti
Traduzione
|
|
Donate to support our development efforts.
|
|
|