Gentoo Logo

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

Indice:

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
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Autore: Daniel Robbins
** Data: 16 Giugno 2000
*/
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
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Autore: Daniel Robbins
** Data: 16 Giugno 2000
**
** Questo insieme di funzioni di coda era originariamente conscio dei thread.
** Io l'ho scritto per fare in modo che questo insieme di routine di coda sia
** thread-ignorante (giusto un generico noioso ma molto veloce insieme di
** routine di coda). Perché questo cambiamento? Perché ha più senso aver il
** supporto ai thread come un add-on opzionale. Si consideri una situazione in
** cui si vogliono aggiungere 5 nodi alla coda. Con la versione con i thread,
** ciascuna chiamata a queue_port() automaticamente bloccherebbe o sbloccherebbe
** il mutex della coda 5 volte-- c'è molto overhead non necessario.
** Tuttavia, muovendo la parte relativa al thread al di fuori delle routine di
** coda, il chiamante può bloccare il mutex una volta all'inizio, dopo di che
** inserisce i cinque oggetti, ed infine lo sblocca. Spostare il codice  di
** blocco/sblocco al di fuori delle funzioni di coda permette ottimizzazioni
** altrimenti impossibili. Rende inoltre questo codice utile ad applicazioni
** senza thread.
**
** Possiamo facilmente rendere thread-enable questa struttura  dati usando il
** tipo data-control definito in control.c e control.h. */
#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
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Autore: Daniel Robbins
** Data: 16 Giugno 2000
**
** Queste routine forniscono un modo semplice per rendere qualsiasi tipo di
** strutture dati thread-aware. Semplicemente associamo una struttura
** data-control con la struttura dati (creando una nuova struttura, ad
** esempio). Dopo, semplicemente, bloccano e sbloccano il mutex, o
** (aspetta/segnala/trasmette) la variabile di condizione nella struttura
** data_control come necessario.
**
** Le strutture data_control contengono un int chiamato "active". Lo scopo di
** questo int è di essere usato per uno specifico tipo di progetto
** multi-threaded, dove ciascun thread  controlla lo stato dell'"active" ogni
** volta che blocca il mutex. Se "active" è 0 il thread sa che invece di fare
** la sua normale routine deve a sua volta fermarsi.
** Se active è 1, dovrebbe continuare come sempre. Così, impostando active a
** 0, un thread di controllo può facilmente informare un thread work crew di
** spegnersi invece che processare nuovi job. Si usino le funzione
** control_activate() e control_deactivate(), che trasmetteranno anche sulla
** variabile di condizione della struttura data_control, così che tutti i
** thread fermi in pthread_cond_wait() si sveglino, abbiano modo di notare il
** cambiamento ed infine terminino.*/
#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"
/* Il work_queue tiene i task per i differenti thread da
completare.*/
struct work_queue {
  data_control control;
  queue work;
} wq;
/* Ho aggiunto un numero di job al work node. Normalmente il work node
contiene ulteriori dati che hanno bisogno  di essere processati.*/
typedef struct work_node {
  struct node *next;
  int jobnum;
} wnode;
/* La coda di cleanup conserva i thread formati. Prima che un threa
d termini aggiunge se stesso a questa lista. Siccome il thread principale
sta aspettando le modifiche di questa lista, si sveglia e "pulisce" il
thread appena terminato. */
struct cleanup_queue {
  data_control control;
  queue cleanup;
} cq;
/* Ho aggiunto un numero al thread (ad uso debugging/studio) e una
thread id al nodo cleanup. Il nodo cleanup  viene passato al nuovo thread
nell'avvio, e un attimo prima che il thread si fermi, attacca il nodo cleanup
alla coda di cleanup. Il thread principale monitora la coda di cleanup  ed è
quello che esegue il necessario cleanup. */
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);
    /* sotto, dormiamo fino a che c'è veramente un nodo cleanup.
Questo si prende cura di ogni falso risveglio ... Anche se usciamo da
pthread_cond_wait(), non riteniamo che la condizione che stiamo
aspettando sia
 vera.*/
    while (cq.cleanup.head==NULL) {
      pthread_cond_wait(&cq.control.cond,&cq.control.mutex);
    }
    /* a questo punto, conserviamo il mutex e c'è un item nella lista
che dobbiamo processare. Per prima cosa  rimuoviamo il nodo dalla
coda. Poi, chiamiamo pthread_join() sulla parte immagazzinata nel node. Quando
pthread_join() ritorna, è tutto pulito dopo il thread. Solo allora facciamo
 un free() al node, decrementiamo il numero di thread addizionali
che dobbiamo aspettare e ripetiamo l'intero processo, se necessario */
      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.


Stampa

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.

Copyright 2001-2014 Gentoo Foundation, Inc. Questions, Comments? Contact us.