Pthreads/fr: Difference between revisions

From Alliance Doc
Jump to navigation Jump to search
No edit summary
No edit summary
Tags: Mobile edit Mobile web edit
 
(58 intermediate revisions by 2 users not shown)
Line 2: Line 2:


=Introduction=
=Introduction=
Le terme ''pthreads'' provient de [https://fr.wikipedia.org/wiki/Threads_POSIX POSIX threads], l'une des premières techniques de parallélisation. Tout comme [[OpenMP]], pthreads s'emploie dans un contexte de mémoire partagée et donc habituellement sur un seul nœud où le nombre de fils d'exécution actifs est limité aux cœurs CPU disponibles. On utilise pthreads dans plusieurs langages de programmation, mais surtout en C. En Fortran, la parallélisation de fils d'exécution se fait préférablement avec OpenMP alors qu'en C++, les outils de la bibliothèque [http://www.boost.org Boost] sont mieux adaptés.
Le terme ''pthreads'' provient de [https://fr.wikipedia.org/wiki/Threads_POSIX POSIX threads], l'une des premières techniques de parallélisation. Tout comme les autres outils faisant usage de fils d'exécution, pthreads s'emploie dans un contexte de mémoire partagée et donc habituellement sur un seul nœud où le nombre de fils actifs est limité aux cœurs CPU disponibles sur ce nœud. On utilise pthreads dans plusieurs langages de programmation, mais surtout en C. En Fortran, la parallélisation de fils d'exécution se fait préférablement avec [[OpenMP/fr|OpenMP]]. En C++, les outils de la bibliothèque [http://www.boost.org Boost] issus de la [https://fr.wikipedia.org/wiki/C%2B%2B11 norme C11] sont mieux adaptés à la programmation orientée-objet.


pthreads a servi de base aux approches de parallélisation qui ont suivi, dont OpenMP. On peut voir pthreads comme étant un ensemble d'outils primitifs offrant des fonctionnalités élémentaires de parallélisation, contrairement aux APIs conviviales et de haut niveau comme OpenMP. Dans le modèle pthreads, les fils sont générés dynamiquement pour exécuter des sous-procédures dites légères qui exécutent les opérations de façon asynchrone; ces fils sont ensuite détruits après avoir été réintégrés au processus principal. Puisque tous les fils d'un même programme résident dans le même espace mémoire, il est facile de partager les données à l'aide de variables globales, contrairement à une approche distribuée comme [[MPI]]; toute modification aux données partagées risque cependant de créer de la concurrence (''race conditions'').
La bibliothèque pthreads a servi de base aux approches de parallélisation qui ont suivi, dont OpenMP. On peut voir pthreads comme étant un ensemble d'outils primitifs offrant des fonctionnalités élémentaires de parallélisation, contrairement aux APIs conviviales et de haut niveau comme OpenMP. Dans le modèle pthreads, les fils sont générés dynamiquement pour exécuter des sous-procédures dites légères qui exécutent les opérations de façon asynchrone; ces fils sont ensuite détruits après avoir réintégré le processus principal. Puisque tous les fils d'un même programme résident dans le même espace mémoire, il est facile de partager les données à l'aide de variables globales, contrairement à une approche distribuée comme [[MPI]]; toute modification aux données partagées risque cependant de créer des [https://en.wikipedia.org/wiki/Race_condition situations de compétition]  (''race conditions'').
 
Pour paralléliser un programme avec pthreads ou toute autre technique, il importe de considérer la capacité du programme à s'exécuter en parallèle, ce que nous appellerons sa [[scalability/fr|scalabilité]]. Après avoir parallélisé votre locigiel et que sa qualité vous satisfait, nous vous recommandons d'effectuer une analyse de sa scalabilité pour en comprendre la performance.


<div class="mw-translate-fuzzy">
=Compilation=
=Compilation=
Pour utiliser les fonctions et structures de données associées à pthreads dans votre programme C, il faut y inclure le fichier entête (''header file'') <tt>pthread.h</tt> et compiler le programme avec un indicateur (''flag'') pour faire le lien avec la bibliothèque pthreads.
Pour utiliser les fonctions et structures de données associées à pthreads dans votre programme C, il faut y inclure le fichier d'en-tête (''header file'') <tt>pthread.h</tt> et compiler le programme avec un indicateur (''flag'') pour faire le lien avec la bibliothèque pthreads.
</div>
{{Command|gcc -pthread -o test threads.c
}}
Le nombre de fils pour le programme est défini par une des méthodes suivantes&nbsp;:
* utilisé comme argument dans une ligne de commande;
* entré via une variable d'environnement;
* encodé dans le fichier source (ceci ne permet toutefois pas d'ajuster le nombre de fils à l'exécution).


=Creation and Destruction of Pthreads=
=Création et destruction des pthreads=  
When parallelizing an existing serial program using pthreads, we use a programming model where threads are created by a parent, then carry out some work, and finally are reabsorbed or joined back into the parent. The parent may be the serial ''master thread'' or another ''worker thread''.
Pour paralléliser avec pthreads un programme séquentiel existant, nous utilisons un modèle de programmation où les fils sont créés par un parent, exécutent une partie du travail, puis sont réintégrés au parent. Le parent est soit le ''fil maître'' séquentiel ou un des autres ''fils esclaves''.


New threads are created with the function <tt>[http://pubs.opengroup.org/onlinepubs/009695399/functions/pthread_create.html pthread_create]</tt>.  This function has four arguments:
La fonction <tt>[http://pubs.opengroup.org/onlinepubs/009695399/functions/pthread_create.html pthread_create]</tt> crée des nouveaux fils avec ces quatre arguments&nbsp;:
*the unique identifier for the newly created thread;
*l'identifiant unique pour le nouveau fil;
*the set of attributes for this thread;
*l'ensemble des attributs du fil;
*the C function that the thread will execute upon initiation (the "start routine");
*la fonction C que le fil exécute lorsqu'il est amorcé (la routine de lancement);
*the argument for the start routine.
*l'argument de la routine de lancement.
{{File
{{File
   |name=thread.c
   |name=thread.c
Line 53: Line 59:
}
}
}}
}}
This simple example creates twelve threads, each one executing the function <tt>task</tt> with the argument consisting of the thread's index, from 0 to 11. Note that the call of <tt>pthread_create</tt> is non-blocking, i.e. the root or master thread, which is executing the <tt>main</tt> function, continues to execute after each of the twelve worker threads is created. After creating the twelve threads, the master thread then goes into the second ''for'' loop and calls <tt>[http://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_join.html pthread_join]</tt>, a blocking function where the master thread waits for the twelve workers to finish executing the function <tt>task</tt> and rejoin the master thread. While trivial, this program illustrates the basic lifecycle of a POSIX thread: the master thread creates a thread by assigning it a function to run, then waits for the thread to finish and join back into the execution of the master thread.
Dans cet exemple, l'index du fil (de 0 à 11) est passé en argument;  la fonction <tt>task</tt> est donc exécutée par chacun des 12 fils. Remarquez que  la fonction <tt>pthread_create</tt> ne bloque pas le  fil maître, qui continue à exécuter la fonction <tt>main</tt> après la création de chacun des fils. Une fois les 12 fils créés, le fil maître entre dans la deuxième boucle ''for'' et appelle la fonction bloquante <tt>[http://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_join.html pthread_join]</tt>&nbsp;: le fil maître attend alors que les 12 fils esclaves terminent l'exécution de la fonction <tt>task</tt> et qu'ils réintègrent ensuite le fil maître. Cet exemple simple illustre bien le fonctionnement de base d'un fil POSIX&nbsp;: le fil maître crée un fil en lui assignant une fonction à exécuter et attend ensuite que le fil créé termine cette fonction, puis réintégre le fil maître.


If you run this test program several times in a row you'll likely notice that the order in which you see the various worker threads saying hello varies, which is what we would expect since they are now running in an asynchronous manner. Each time the program is executed, the twelve threads compete for access to the standard output during the <tt>printf</tt> call and from one execution of the program to another the winners of this competition will change.
En exécutant ce code plusieurs fois de suite, vous noterez probablement une variation dans l'ordre dans lequel les fils esclaves disent ''hello'', ce qui est prévisible puisqu'ils s'exécutent en mode asynchrone. Chaque fois que le programme est exécuté, les 12 fils répondent en même temps à la fonction <tt>printf</tt> et ce n'est jamais le même fil qui remporte la course.


=Synchronizing Data Access=
=Synchronisation de l'accès aux données=  
In a more realistic program, worker threads will need to read and eventually modify certain data in order to accomplish their tasks. These data normally consist of a set of global variables of different types and dimensions, and with multiple threads reading and writing these data, we need to ensure that the access to these data is synchronized in some fashion to avoid [https://en.wikipedia.org/wiki/Race_condition race conditions], i.e. situations in which the program's output depends on the random order in which the asynchronous threads access the data. Typically, we want the parallel version of our program to produce results identical to what we would obtain when running it serially, so the race conditions are unacceptable.
Dans un programme réel, les fils esclaves doivent lire et dans certains cas modifier les données afin d'accomplir leurs tâches. Ces données sont habituellement un ensemble de variables globales de divers types et dimensions; l'accès concurrent en lecture et en écriture par plusieurs fils doit donc être synchronisé afin d'éviter les [https://en.wikipedia.org/wiki/Race_condition situations de compétition], c'est-à-dire les cas où le résultat du programme dépend de l'ordre dans lequel les fils esclaves accèdent aux données. Si un programme en parallèle doit donner le même résultat que sa version en série, les situations de compétition ne doivent pas se produire.


The simplest and most common way to control the reading and writing of data shared among threads is the [https://en.wikipedia.org/wiki/Lock_(computer_science) mutex], derived from the expression 'mutual exclusion'. In pthreads, a mutex is a kind of variable that may be "locked" or "owned" by only one thread at a time. The thread must then release or unlock the mutex once the global data has been read or modified. The code that lies between the call to lock a mutex and the call to unlock it will only be executed by a single thread at a time. To create a mutex in a pthreads program, we declare a global variable of type <tt>pthread_mutex_t</tt> which must be initialized before it is used by calling <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_mutex_init.html pthread_mutex_init]</tt>. At the program's end we release the resources associated with the mutex by calling <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_mutex_init.html pthread_mutex_destroy]</tt>.
Le moyen le plus simple et le plus utilisé pour contrôler l'accès concurrent est le [https://fr.wikipedia.org/wiki/Verrou_(informatique) verrou]; dans le contexte de pthreads, le mécanisme de verrouillage est le mutex (pour ''mutual exclusion''). Les variables de ce type sont assignées à un seul fil à la fois. Après la lecture ou la modification, le fil désactive le verrou. Le code entre l'appel de la variable et le moment où elle est désactivée est exécuté exclusivement par ce fil. Pour créer un mutex, il faut déclarer une variable globale de type <tt>pthread_mutex_t</tt>. Cette variable est initialisée par la fonction <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_mutex_init.html pthread_mutex_init]</tt>. À la fin du programme, les ressources sont ''déverrouillées'' par la fonction <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_mutex_init.html pthread_mutex_destroy]</tt>.
{{File
{{File
   |name=thread_mutex.c
   |name=thread_mutex.c
Line 105: Line 111:
}
}
}}
}}
In this example, based on the previous code, access to the standard output channel is serialized - as it normally should be - using a mutex. The call to <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_mutex_lock.html pthread_mutex_lock]</tt> is ''blocking'', i.e. the thread will continue to wait indefinitely for the mutex to become available, so you have to take care that no deadlock can occur in your code, that is, that the mutex is guaranteed to become available eventually. This is particularly problematic in a more realistic example where you may have many different mutexes designed to control access to different global data structures. There is also a non-blocking alternative, <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_mutex_lock.html pthread_mutex_trylock]</tt>, which if it fails to obtain the mutex lock returns immediately with a non-zero value indicating that the mutex is busy. You should also ensure that no extraneous code appears inside the serialized code block; since this code will be executed in a serial manner, you want it to be as short as it can safely be in order not to reduce your program's parallel performance.
Dans cet exemple basé sur le contenu du fichier ''thread.c'' plus haut, l'accès au canal de sortie standard est sérialisé comme il se doit avec un mutex. L'appel de <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_mutex_lock.html pthread_mutex_lock]</tt> effectue le blocage, c'est-à-dire que le fil attendra indéfiniment que le mutex devienne disponible. Il faut s'assurer que le code ne provoque pas d'autre blocage puisque le mutex doit éventuellement devenir disponible. Ceci pose problème surtout dans un programme réel qui comporte plusieurs variables mutex contrôlant l'accès à différentes structures de données globales.
<br />
Dans le cas de l'alternative non bloquante <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_mutex_lock.html pthread_mutex_trylock]</tt>, la valeur non nulle est immédiatement produite si le mutex n'est pas accompli, indiquant ainsi que le mutex est occupé. Il faut aussi s'assurer qu'il n'y a pas de code superflu à l'intérieur du bloc sérialisé; puisque ce code est exécuté en série, il doit être le plus concis possible afin de ne pas nuire au parallélisme dans l'exécution du programme.


A more subtle form of data synchronization is possible with the read/write lock, <tt>pthread_rwlock_t</tt>. With this construct, multiple threads can simultaneously read the value of a variable but for write access, the read/write lock behaves like the standard mutex, i.e. no other thread may have have any access (read or write) to the variable. Like with a mutex, a <tt>pthread_rwlock_t</tt> must be initialized before its first use and destroyed when it is no longer needed during the program. Individual threads can obtain either a read lock by calling <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_rwlock_rdlock.html pthread_rwlock_rdlock]</tt>, or a write lock with <tt>[http://pubs.opengroup.org/onlinepubs/007908775/xsh/pthread_rwlock_wrlock.html pthread_rwlock_wrlock]</tt>. Either one is released using <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_rwlock_unlock.html pthread_rwlock_unlock]</tt>.
Une synchronisation plus subtile est possible avec le verrou lecture/écriture <tt>pthread_rwlock_t</tt>. Cet outil permet la lecture simultanée d'une variable par plusieurs fils, mais se comporte comme un mutex standard, c'est-à-dire qu'aucun autre fil n'a accès à cette variable (en lecture ou en écriture). Comme pour le mutex, le verrou <tt>pthread_rwlock_t</tt> doit être initialisé avant son utilisation et détruit quand il n'est plus nécessaire. Un fil obtient un verrou en lecture avec  <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_rwlock_rdlock.html pthread_rwlock_rdlock]</tt> et un verrou en écriture avec <tt>[http://pubs.opengroup.org/onlinepubs/007908775/xsh/pthread_rwlock_wrlock.html pthread_rwlock_wrlock]</tt>. Dans les deux cas, le verrou est détruit avec <tt>[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_rwlock_unlock.html pthread_rwlock_unlock]</tt>.


Another construct is used to allow multiple threads to wait for a single condition, for example waiting for work to become available for the worker threads. This construct is called a ''condition variable'' and has the datatype <tt>pthread_cond_t</tt>. Like a mutex or read/write lock, a condition variable must be initialized before its first use and destroyed when it is no longer needed. The use of a condition variable also requires a mutex to control access to the variable(s) that are the basis for the condition that is being tested. A thread that needs to wait on a condition will lock the mutex and then call the function <tt>[http://pubs.opengroup.org/onlinepubs/007908775/xsh/pthread_cond_wait.html pthread_cond_wait]</tt> with two arguments: the condition variable, and the mutex. The mutex will be released ''atomically'' with the creation of the condition variable that the thread is now waiting upon, so that other threads can lock the mutex either to wait on the same condition or to modify one or more variables, thereby changing the condition.
Un autre outil permet à plusieurs fils d'agir sur la même condition, par exemple d'attendre que les fils esclaves soient sollicités pour une tâche. Il s'agit d'une variable-condition, exprimée comme suit : <tt>pthread_cond_t</tt>. Comme le mutex ou le verrou lecture/écriture, la variable-condition doit être initialisée avant son utilisation et détruite lorsqu'elle n'est plus nécessaire. Pour utiliser cette variable-condition, un mutex doit contrôler l'accès aux variables qui ont une incidence sur la condition. Un fil en attente d'une condition verrouille le mutex et fait appel à la fonction <tt>[http://pubs.opengroup.org/onlinepubs/007908775/xsh/pthread_cond_wait.html pthread_cond_wait]</tt> avec la variable-condition et le mutex en arguments. Le mutex est détruit ''atomiquement'' avec la création de la variable-condition dont le résultat est attendu par le fil; les autres fils peuvent alors verrouiller le mutex, soit pour attendre la condition ou pour modifier une ou plusieurs de variables, ce qui modifiera la condition.
{{File
{{File
   |name=thread_condition.c
   |name=thread_condition.c
Line 188: Line 196:
}
}
}}
}}
In the above example we have two worker threads which modify the value of the integer <tt>workload</tt>, whose initial value must be less than or equal to 25. The first thread locks the mutex and then waits because <tt>workload <= 25</tt>, creating the condition variable <tt>ticker</tt> and releasing the mutex. The second thread can then perform a loop that increments the value of <tt>workload</tt> by three at each iteration. After each increment the second thread checks if the <tt>workload</tt> is greater than 25, and when it is, calls <tt>[http://pubs.opengroup.org/onlinepubs/007908799/xsh/pthread_cond_signal.html pthread_cond_signal]</tt> to alert the thread waiting on <tt>ticker</tt> that the condition is now satisfied. If there were more than one thread waiting on <tt>ticker</tt> we could instead use <tt>[http://pubs.opengroup.org/onlinepubs/009695399/functions/pthread_cond_broadcast.html pthread_cond_broadcast]</tt> to notify ''all'' waiting threads that the condition is satisfied.  With the first thread signalled, the second thread sets the exit condition for the loop, releases the mutex, and disappears in the <tt>pthread_join</tt>. Meanwhile the first thread, having been woken up, increments <tt>workload</tt> by 15 and exits the function <tt>task</tt> itself. After the worker threads have been absorbed, the master thread prints out the final value of <tt>workload</tt> and the program exits.
Dans cet exemple, deux fils esclaves modifient la valeur de l'entier <tt>workload</tt> dont la valeur initiale doit être plus petite ou égale à 25. Le premier fil verrouille le mutex et attend parce que <tt>workload <= 25</tt>; la variable-condition <tt>ticker</tt> est créée et le mutex est détruit. Le deuxième fil peut alors exécuter la boucle, qui elle incrémente de trois la valeur de <tt>workload</tt> à chaque itération. À chaque incrémentation, le deuxième fil vérifie si la valeur de <tt>workload</tt> est plus grande que 25; si c'est le cas, le fil appelle <tt>[http://pubs.opengroup.org/onlinepubs/007908799/xsh/pthread_cond_signal.html pthread_cond_signal]</tt> pour signaler au fil en attente que la condition est satisfaite. Une fois que le signal est reçu par le premier fil, le deuxième fil fixe la condition de sortie de la boucle, amorce le mutex et disparait avec <tt>pthread_join</tt>. Entretemps, le premier fil étant ''réveillé'', celui-ci incrémente de 15 la valeur de <tt>workload</tt> et quitte la fonction <tt>task</tt>. Quand tous les fils esclaves sont réintégrés, le fil maître imprime la valeur finale de <tt>workload</tt> et le programme se termine.
 
De façon générale, dans un programme réel où plusieurs fils sont en attente d'une variable-condition, la fonction pthread_cond_broadcast signale à tous les fils en attente que la condition est satisfaite. Dans ce contexte, pthread_cond_signal avertirait un seul fil au hasard et les autres fils demeureraient en attente.


=Further Reading=
=Pour en savoir plus=  
This page is only intended to provide a very brief overview of what is in fact a complex and demanding subject. Individuals who are interested in a more in-depth discussion of pthreads, the various optional arguments that are available for many function calls - where we have used the default NULL argument for such parameters in this page - and advanced topics can consult sources like David Butenhof's Programming with POSIX Threads (Addison-Wesley, 1993) or the excellent [https://computing.llnl.gov/tutorials/pthreads LANL tutorial].
Pour plus d'information sur pthreads, sur les arguments optionnels pour les diverses fonctions (les paramètres utilisés dans cette page utilisent l'argument par défaut NULL) et sur les sujets de niveau avancé, nous recommandons l'ouvrage de David Butenhof,  [https://ptgmedia.pearsoncmg.com/images/9780201633924/samplepages/0201633922.pdf Programming with POSIX Threads] ou l'excellent [https://computing.llnl.gov/tutorials/pthreads tutoriel du Lawrence Livermore National Laboratory].

Latest revision as of 18:09, 8 May 2023

Other languages:

Introduction

Le terme pthreads provient de POSIX threads, l'une des premières techniques de parallélisation. Tout comme les autres outils faisant usage de fils d'exécution, pthreads s'emploie dans un contexte de mémoire partagée et donc habituellement sur un seul nœud où le nombre de fils actifs est limité aux cœurs CPU disponibles sur ce nœud. On utilise pthreads dans plusieurs langages de programmation, mais surtout en C. En Fortran, la parallélisation de fils d'exécution se fait préférablement avec OpenMP. En C++, les outils de la bibliothèque Boost issus de la norme C11 sont mieux adaptés à la programmation orientée-objet.

La bibliothèque pthreads a servi de base aux approches de parallélisation qui ont suivi, dont OpenMP. On peut voir pthreads comme étant un ensemble d'outils primitifs offrant des fonctionnalités élémentaires de parallélisation, contrairement aux APIs conviviales et de haut niveau comme OpenMP. Dans le modèle pthreads, les fils sont générés dynamiquement pour exécuter des sous-procédures dites légères qui exécutent les opérations de façon asynchrone; ces fils sont ensuite détruits après avoir réintégré le processus principal. Puisque tous les fils d'un même programme résident dans le même espace mémoire, il est facile de partager les données à l'aide de variables globales, contrairement à une approche distribuée comme MPI; toute modification aux données partagées risque cependant de créer des situations de compétition (race conditions).

Pour paralléliser un programme avec pthreads ou toute autre technique, il importe de considérer la capacité du programme à s'exécuter en parallèle, ce que nous appellerons sa scalabilité. Après avoir parallélisé votre locigiel et que sa qualité vous satisfait, nous vous recommandons d'effectuer une analyse de sa scalabilité pour en comprendre la performance.

Compilation

Pour utiliser les fonctions et structures de données associées à pthreads dans votre programme C, il faut y inclure le fichier d'en-tête (header file) pthread.h et compiler le programme avec un indicateur (flag) pour faire le lien avec la bibliothèque pthreads.

Question.png
[name@server ~]$ gcc -pthread -o test threads.c

Le nombre de fils pour le programme est défini par une des méthodes suivantes :

  • utilisé comme argument dans une ligne de commande;
  • entré via une variable d'environnement;
  • encodé dans le fichier source (ceci ne permet toutefois pas d'ajuster le nombre de fils à l'exécution).

Création et destruction des pthreads

Pour paralléliser avec pthreads un programme séquentiel existant, nous utilisons un modèle de programmation où les fils sont créés par un parent, exécutent une partie du travail, puis sont réintégrés au parent. Le parent est soit le fil maître séquentiel ou un des autres fils esclaves.

La fonction pthread_create crée des nouveaux fils avec ces quatre arguments :

  • l'identifiant unique pour le nouveau fil;
  • l'ensemble des attributs du fil;
  • la fonction C que le fil exécute lorsqu'il est amorcé (la routine de lancement);
  • l'argument de la routine de lancement.
File : thread.c

#include <stdio.h>
#include <pthread.h>

const long NT = 12;

void* task(void* thread_id)
{
  long tnumber = (long) thread_id; 
  printf("Hello World from thread %ld\n",1+tnumber);
}

int main(int argc,char** argv)
{
  int success;
  long i;
  pthread_t threads[NT];

  for(i=0; i<NT; ++i) {
    success = pthread_create(&threads[i],NULL,task,(void*)i);
    if (success != 0) {
      printf("ERROR: Unable to create worker thread %ld successfully\n",i);
      return 1;
    }
  }
  for(i=0; i<NT; ++i) {
    pthread_join(threads[i],NULL);
  }
  return 0;
}


Dans cet exemple, l'index du fil (de 0 à 11) est passé en argument; la fonction task est donc exécutée par chacun des 12 fils. Remarquez que la fonction pthread_create ne bloque pas le fil maître, qui continue à exécuter la fonction main après la création de chacun des fils. Une fois les 12 fils créés, le fil maître entre dans la deuxième boucle for et appelle la fonction bloquante pthread_join : le fil maître attend alors que les 12 fils esclaves terminent l'exécution de la fonction task et qu'ils réintègrent ensuite le fil maître. Cet exemple simple illustre bien le fonctionnement de base d'un fil POSIX : le fil maître crée un fil en lui assignant une fonction à exécuter et attend ensuite que le fil créé termine cette fonction, puis réintégre le fil maître.

En exécutant ce code plusieurs fois de suite, vous noterez probablement une variation dans l'ordre dans lequel les fils esclaves disent hello, ce qui est prévisible puisqu'ils s'exécutent en mode asynchrone. Chaque fois que le programme est exécuté, les 12 fils répondent en même temps à la fonction printf et ce n'est jamais le même fil qui remporte la course.

Synchronisation de l'accès aux données

Dans un programme réel, les fils esclaves doivent lire et dans certains cas modifier les données afin d'accomplir leurs tâches. Ces données sont habituellement un ensemble de variables globales de divers types et dimensions; l'accès concurrent en lecture et en écriture par plusieurs fils doit donc être synchronisé afin d'éviter les situations de compétition, c'est-à-dire les cas où le résultat du programme dépend de l'ordre dans lequel les fils esclaves accèdent aux données. Si un programme en parallèle doit donner le même résultat que sa version en série, les situations de compétition ne doivent pas se produire.

Le moyen le plus simple et le plus utilisé pour contrôler l'accès concurrent est le verrou; dans le contexte de pthreads, le mécanisme de verrouillage est le mutex (pour mutual exclusion). Les variables de ce type sont assignées à un seul fil à la fois. Après la lecture ou la modification, le fil désactive le verrou. Le code entre l'appel de la variable et le moment où elle est désactivée est exécuté exclusivement par ce fil. Pour créer un mutex, il faut déclarer une variable globale de type pthread_mutex_t. Cette variable est initialisée par la fonction pthread_mutex_init. À la fin du programme, les ressources sont déverrouillées par la fonction pthread_mutex_destroy.

File : thread_mutex.c

#include <stdio.h>
#include <pthread.h>

const long NT = 12;

pthread_mutex_t mutex;

void* task(void* thread_id)
{
  long tnumber = (long) thread_id; 
  pthread_mutex_lock(&mutex);
  printf("Hello World from thread %ld\n",1+tnumber);
  pthread_mutex_unlock(&mutex);
}

int main(int argc,char** argv)
{
  int success;
  long i;
  pthread_t threads[NT];

  pthread_mutex_init(&mutex,NULL);

  for(i=0; i<NT; ++i) {
    success = pthread_create(&threads[i],NULL,task,(void*)i);
    if (success != 0) {
      printf("ERROR: Unable to create worker thread %ld successfully\n",i);
      pthread_mutex_destroy(&mutex);
      return 1;
    }
  }
  for(i=0; i<NT; ++i) {
    pthread_join(threads[i],NULL);
  }

  pthread_mutex_destroy(&mutex);

  return 0;
}


Dans cet exemple basé sur le contenu du fichier thread.c plus haut, l'accès au canal de sortie standard est sérialisé comme il se doit avec un mutex. L'appel de pthread_mutex_lock effectue le blocage, c'est-à-dire que le fil attendra indéfiniment que le mutex devienne disponible. Il faut s'assurer que le code ne provoque pas d'autre blocage puisque le mutex doit éventuellement devenir disponible. Ceci pose problème surtout dans un programme réel qui comporte plusieurs variables mutex contrôlant l'accès à différentes structures de données globales.
Dans le cas de l'alternative non bloquante pthread_mutex_trylock, la valeur non nulle est immédiatement produite si le mutex n'est pas accompli, indiquant ainsi que le mutex est occupé. Il faut aussi s'assurer qu'il n'y a pas de code superflu à l'intérieur du bloc sérialisé; puisque ce code est exécuté en série, il doit être le plus concis possible afin de ne pas nuire au parallélisme dans l'exécution du programme.

Une synchronisation plus subtile est possible avec le verrou lecture/écriture pthread_rwlock_t. Cet outil permet la lecture simultanée d'une variable par plusieurs fils, mais se comporte comme un mutex standard, c'est-à-dire qu'aucun autre fil n'a accès à cette variable (en lecture ou en écriture). Comme pour le mutex, le verrou pthread_rwlock_t doit être initialisé avant son utilisation et détruit quand il n'est plus nécessaire. Un fil obtient un verrou en lecture avec pthread_rwlock_rdlock et un verrou en écriture avec pthread_rwlock_wrlock. Dans les deux cas, le verrou est détruit avec pthread_rwlock_unlock.

Un autre outil permet à plusieurs fils d'agir sur la même condition, par exemple d'attendre que les fils esclaves soient sollicités pour une tâche. Il s'agit d'une variable-condition, exprimée comme suit : pthread_cond_t. Comme le mutex ou le verrou lecture/écriture, la variable-condition doit être initialisée avant son utilisation et détruite lorsqu'elle n'est plus nécessaire. Pour utiliser cette variable-condition, un mutex doit contrôler l'accès aux variables qui ont une incidence sur la condition. Un fil en attente d'une condition verrouille le mutex et fait appel à la fonction pthread_cond_wait avec la variable-condition et le mutex en arguments. Le mutex est détruit atomiquement avec la création de la variable-condition dont le résultat est attendu par le fil; les autres fils peuvent alors verrouiller le mutex, soit pour attendre la condition ou pour modifier une ou plusieurs de variables, ce qui modifiera la condition.

File : thread_condition.c

#include <stdio.h>
#include <pthread.h>

const long NT = 2;

pthread_mutex_t mutex;
pthread_cond_t ticker;

int workload;

void* task(void* thread_id)
{
  long tnumber = (long) thread_id;

  if (tnumber == 0) {
    pthread_mutex_lock(&mutex);
    while(workload <= 25) {
      pthread_cond_wait(&ticker,&mutex);
    }
    printf("Thread %ld: incrementing workload by 15\n",1+tnumber);
    workload += 15;
    pthread_mutex_unlock(&mutex);
  }
  else {
    int done = 0;
    do {
      pthread_mutex_lock(&mutex);
      workload += 3;
      printf("Thread %ld: current workload is %d\n",1+tnumber,workload);
      if (workload > 25) {
        done = 1;
        pthread_cond_signal(&ticker);
      }
      pthread_mutex_unlock(&mutex);
    } while(!done);
  }
}

int main(int argc,char** argv)
{
  int success;
  long i;
  pthread_t threads[NT];

  workload = atoi(argv[1]);
  if (workload > 25) {
    printf("Initial workload must be <= 25, exiting...\n");
    return 0;
  }

  pthread_mutex_init(&mutex,NULL);
  pthread_cond_init(&ticker,NULL);

  for(i=0; i<NT; ++i) {
    success = pthread_create(&threads[i],NULL,task,(void*)i);
    if (success != 0) {
      printf("ERROR: Unable to create worker thread %ld successfully\n",i);
      pthread_mutex_destroy(&mutex);
      return 1;
    }
  }

  for(i=0; i<NT; ++i) {
    pthread_join(threads[i],NULL);
  }

  printf("Final workload is %d\n",workload);

  pthread_cond_destroy(&ticker);
  pthread_mutex_destroy(&mutex);

  return 0;
}


Dans cet exemple, deux fils esclaves modifient la valeur de l'entier workload dont la valeur initiale doit être plus petite ou égale à 25. Le premier fil verrouille le mutex et attend parce que workload <= 25; la variable-condition ticker est créée et le mutex est détruit. Le deuxième fil peut alors exécuter la boucle, qui elle incrémente de trois la valeur de workload à chaque itération. À chaque incrémentation, le deuxième fil vérifie si la valeur de workload est plus grande que 25; si c'est le cas, le fil appelle pthread_cond_signal pour signaler au fil en attente que la condition est satisfaite. Une fois que le signal est reçu par le premier fil, le deuxième fil fixe la condition de sortie de la boucle, amorce le mutex et disparait avec pthread_join. Entretemps, le premier fil étant réveillé, celui-ci incrémente de 15 la valeur de workload et quitte la fonction task. Quand tous les fils esclaves sont réintégrés, le fil maître imprime la valeur finale de workload et le programme se termine.

De façon générale, dans un programme réel où plusieurs fils sont en attente d'une variable-condition, la fonction pthread_cond_broadcast signale à tous les fils en attente que la condition est satisfaite. Dans ce contexte, pthread_cond_signal avertirait un seul fil au hasard et les autres fils demeureraient en attente.

Pour en savoir plus

Pour plus d'information sur pthreads, sur les arguments optionnels pour les diverses fonctions (les paramètres utilisés dans cette page utilisent l'argument par défaut NULL) et sur les sujets de niveau avancé, nous recommandons l'ouvrage de David Butenhof, Programming with POSIX Threads ou l'excellent tutoriel du Lawrence Livermore National Laboratory.