Le carnet On est
Cours 4 · encre prune

Les threads

Processus légers POSIX : pthread_create, pthread_join, pthread_exit, pthread_detach, mutex et sections critiques. Compilation avec -lpthread.

~ 25 min de lecture TP — pthread / mutex 12 flashcards 7 quiz

📄 Ressources du cours

📥 Télécharger les slides  ·  🔗 Ouvrir dans un nouvel onglet

🎥 Vidéos recommandées

  • 🇫🇷 Processus et Threads

    Tarik3 Med3 · 48:30 · 28k vues — AboutPressCopyrightContact usCreatorsAdvertiseDevelopersTermsPrivacyPolicy & SafetyHow YouTube worksTest new features · © 2024 Google LLC

  • 🇫🇷 La programmation système sous Linux: Semaphore et ordonnancement des threads

    Walid SASSI · 10:53 · 11k vues — AboutPressCopyrightContact usCreatorsAdvertiseDevelopersTermsPrivacyPolicy & SafetyHow YouTube worksTest new featuresNFL Sunday Ticket · © 2024 Google LLC

  • 🇬🇧 Mutex Introduction (pthreads) | C Programming Tutorial

    Portfolio Courses · 10:19 · 18k vues — How to use a mutex to prevent race conditions in a multi-threaded C program using the POSIX pthread library. Source code: https://github.com/portfoliocourses...

1. Pourquoi les threads ?

On sait déjà implémenter des algorithmes parallèles avec ce que l'on a vu jusqu'ici (les IPC), mais c'est lourd. Le fork reste un appel système coûteux et la communication entre processus est lente. Les threads (processus légers) viennent corriger ces défauts.

Coût d'un fork — les défauts à corriger Le père et les fils sont des processus indépendants. Chacun a son espace d'adressage et sa pile d'exécution. Le changement de contexte est lent et coûteux. La communication inter-processus est généralement lente. Le partage de données est délicat.
Définition Un processus qui crée des threads ne crée pas d'autres processus : tout se passe dans le même espace d'adressage.
  • Un thread est une « partie » d'un processus.
  • Un processus est l'exécution d'un ensemble (≥ 1) de threads.
  • Chaque thread est associée à une pile d'exécution indépendante.

2. Thread vs Processus

Ce que les threads partagent (et ce qu'ils ne partagent pas)

✅ Partagé entre threads
Variables globales · variables statiques locales · descripteurs de fichiers ouverts · PID · PPID · utilisateurs propriétaires · handlers de signaux · répertoire courant · masque de fichiers.
⛔ Non partagé entre threads
Les identifiants de threads · la pile d'exécution · le masque des signaux.
Conséquence pour le programmeur Ce partage implique la gestion par le programmeur de la concurrence d'accès à ces variables : la synchronisation n'est plus gérée par le système, elle est laissée à l'utilisateur (mutex, sémaphores…).

Tableau comparatif

CritèreProcessus (fork)Thread (pthread)
Espace d'adressageIndépendant (un par processus)Partagé dans le processus
Pile d'exécutionUne par processusUne par thread
PID / PPIDPropres à chaque processusCommuns (tous les threads partagent le PID)
Variables globales / statiquesCopies indépendantesPartagées
Descripteurs de fichiersHérités à la créationPartagés
CréationLourde (appel système)Légère (moins de données à recopier)
Changement de contexteLent / coûteuxPlus facile / rapide
CommunicationIPC (tubes, signaux, mémoire partagée…)Mémoire partagée directe
SynchronisationGérée par le systèmeÀ la charge du programmeur
Attente de finwait / waitpidpthread_join

Avantages / inconvénients des threads

Avantages
  • Partage de mémoire = mécanisme rapide de communication inter-thread.
  • Plus léger : moins de données système à recopier.
  • Plus rapide : context-switch facilité.
Inconvénients (uniquement des difficultés de programmation)
  • Les threads utilisent les mêmes copies des librairies.
  • Il faut gérer la synchronisation (mutex, sémaphores…).
  • Mauvaise synchronisation ⇒ comportement aléatoire : bugs, segfaults, interblocage.

3. Modèle mémoire : un processus, plusieurs threads

Au départ, le processus est constitué d'un unique thread (thread principal) — celui qui exécute la fonction main. La création d'un processus crée un thread natif. Si le thread principal se termine, tous les autres threads du processus se terminent aussi (sauf si l'on utilise pthread_exit dans le main).

Processus (1 seul PID, 1 espace d'adressage) Zone partagée par tous les threads : Segment CODE (texte) Données globales & statiques TAS (heap) — malloc() + descripteurs de fichiers, signaux, cwd… Thread 1 (main) pile + TID + masque sig. Thread 2 pile + TID Thr. 3… pile + TID ↑ accès partagé (sections critiques !)
Où implémente-t-on les threads ?
  • Dans les bibliothèques utilisateur : contrôlés par l'utilisateur — POSIX Pthreads, Java threads.
  • Dans le noyau du SE : contrôlés par le noyau — Windows XP/2000, Solaris, Linux, UNIX, Mac OS X.
  • Et solutions mixtes.

4. Primitives POSIX (récap)

AppelDescription
pthread_createCréer un nouveau thread (fil d'exécution)
pthread_exitTerminer le thread appelant
pthread_joinAttendre la fin d'un autre thread
pthread_mutex_initCréer un mutex
pthread_mutex_destroyDétruire un mutex
pthread_mutex_lockVerrouiller un mutex
pthread_mutex_unlockRelâcher un mutex
Compilation Toujours lier la bibliothèque pthread :
gcc prog.c -o prog -lpthread
Et inclure <pthread.h>.

5. Créer un thread — pthread_create()

#include <pthread.h>

int pthread_create(pthread_t *tid,
                   const pthread_attr_t *attr,
                   void *(*func)(void *),
                   void *arg);
📍 pthread_t *tid
Pointeur vers l'adresse mémoire qui contient le TID (Thread Identifier) du pthread créé.
const pthread_attr_t *attr
Attributs du pthread (taille de la pile, priorité, ressources système…).
attr = NULL ⇒ attributs par défaut (thread joignable, non détaché, ordonnancement normal — pas temps-réel).
🎯 void *(*func)(void *)
La fonction à exécuter par le pthread.
📦 void *arg
Les arguments passés à la fonction func.
Identité d'un pthread
  • Un pthread n'a pas de PID propre : tous les threads partagent le PID du processus.
  • L'identifiant est un objet de type pthread_t : la norme ne dit pas ce qu'il contient (objet opaque).
  • L'appel renvoie 0 en cas de succès, sinon une valeur non nulle identifiant l'erreur.

Erreurs possibles

CodeSignification
EAGAINRessources insuffisantes ou limite de threads atteinte
EINVALParamètres invalides dans attr
EPERMPermissions insuffisantes pour la politique d'ordonnancement / paramètres dans attr

Exemple — création simple

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

void *afficher(void *arg) {
    char *msg = (char *) arg;
    for (int i = 0; i < 5; i++) {
        printf("[thread] %s — itération %d\n", msg, i);
        sleep(1);
    }
    return NULL;
}

int main(void) {
    pthread_t tid;
    char *message = "Bonjour";

    if (pthread_create(&tid, NULL, afficher, (void *) message) != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    /* Sans pthread_join, le main pourrait se terminer
       avant le thread et l'éliminer. */
    pthread_join(tid, NULL);
    printf("[main] thread terminé.\n");
    return 0;
}
/* compilation : gcc prog.c -o prog -lpthread */
Récupération du PID Avec ps aux dans un autre terminal, on ne voit qu'une seule entrée pour le processus (ex. exemple-pthread-create-1). Pour voir les threads, il faut utiliser ps maux.

6. Attributs de thread (pthread_attr_t)

Chaque thread est doté d'un certain nombre d'attributs, regroupés dans un type opaque pthread_attr_t. Ils sont fixés à la création via le 2ème argument de pthread_create.

  • Si les attributs par défaut suffisent, on passe généralement un pointeur NULL.
  • Sinon, on configure préalablement un objet pthread_attr_t.
  • On commence par pthread_attr_init(pthread_attr_t *attr) qui remplit la structure avec les valeurs par défaut.
  • Puis on utilise pthread_attr_getXXX() et pthread_attr_setXXX() pour consulter / modifier l'attribut correspondant.
Exemple — donner un nom à un thread
pthread_setname_np(pthread_self(), "worker-1");

7. Fin d'un thread

Lorsque la fonction principale d'un thread se termine, celui-ci est éliminé. La fonction doit renvoyer une valeur qui pourra être récupérée dans un autre thread. Autre possibilité — la primitive explicite :

void pthread_exit(void *retour);
/* Termine l'exécution du pthread et renvoie `retour`
   (éventuellement NULL). */
Cas particulier — terminaison du main Si main() exécute return ou exit(), tous les threads du processus sont terminés. Pour laisser les autres threads continuer après que le main a fini son rôle, utiliser pthread_exit dans le main (et pas return / exit).

Primitives connexes

int pthread_kill(pthread_t thread, int sig);
/* Envoyer un signal à un thread */

pthread_t pthread_self(void);
/* Retourne l'identificateur du thread appelant */

8. Attendre un thread — pthread_join()

int pthread_join(pthread_t thread, void **retval);
  • Suspend le thread appelant jusqu'à la terminaison du thread désigné (annulé ou terminé via pthread_exit).
  • retval = pointeur sur une variable où le code de retour sera copié.
  • C'est l'équivalent de waitpid pour les processus, sauf qu'on doit spécifier le TID du pthread à attendre.
  • Peut échouer si le TID n'existe pas ou s'il est détaché.
🔁 Exemple typique du cours
« Autant de threads que d'arguments. La fonction de chaque thread calcule le carré et le renvoie. Le thread principal récupère les résultats avec pthread_join et les affiche. »

9. Détachement — pthread_detach()

int pthread_detach(pthread_t tid);
  • Place un thread en cours d'exécution dans l'état détaché (sauf s'il a déjà été joint).
  • Garantit que ses ressources mémoire seront immédiatement libérées à la fin de son exécution.
  • Empêche les autres threads de se synchroniser sur la mort de ce thread via pthread_join.
  • Si on ne veut pas avoir à gérer la fin d'un thread, on le « détache ».
  • Échoue si le thread n'existe pas ou est déjà détaché.
Pourquoi détacher ? Sans détachement, les codes de retour et les piles sont conservés tant que personne n'a joint le thread — risque de saturation ! Avec pthread_detach, pile et code de retour sont libérés dès la fin du thread.

10. Synchronisation : les mutex

Définition Un mutex (MUTual EXclusion) est un objet d'exclusion mutuelle, très pratique pour protéger des données partagées de modifications concurrentes et pour implémenter des sections critiques.
  • Deux états : libre (pris par aucun thread) ou verrouillé (pris par un thread).
  • Un thread qui verrouille un mutex le tient.
  • Un seul thread peut tenir un mutex donné à la fois.
  • En général : variables globales ou locales statiques.

Initialisation

① Statique
pthread_mutex_t mutex =
    PTHREAD_MUTEX_INITIALIZER;
② Dynamique
int pthread_mutex_init(
    pthread_mutex_t *mutex,
    const pthread_mutexattr_t *attr);
mutex : pointeur vers la variable à initialiser.
attr : pointeur vers un pthread_mutexattr_t (ou NULL pour les valeurs par défaut).

Verrouillage / déverrouillage / libération

int pthread_mutex_lock(pthread_mutex_t *mutex);
/* Si mutex libre : verrouillé et attribué au thread appelant.
   Sinon : blocage jusqu'à libération, puis verrouillé. */

int pthread_mutex_unlock(pthread_mutex_t *mutex);
/* Déverrouille le mutex. */

int pthread_mutex_destroy(pthread_mutex_t *mutex);
/* Détruit un mutex, libérant ses ressources.
   Le mutex doit être déverrouillé. */
Piège classique — interblocage Il faut éviter de verrouiller deux fois le même mutex dans le même thread sans le déverrouiller entre-temps : risque de blocage définitif du thread.

Exemple — compteur partagé protégé par un mutex

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

#define N_THREADS  4
#define N_ITER     100000

static long compteur = 0;                       /* variable PARTAGÉE */
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

void *incrementer(void *arg) {
    for (int i = 0; i < N_ITER; i++) {
        pthread_mutex_lock(&mtx);              /* entrée section critique */
        compteur++;                             /* accès protégé */
        pthread_mutex_unlock(&mtx);            /* sortie section critique */
    }
    return NULL;
}

int main(void) {
    pthread_t th[N_THREADS];

    for (int i = 0; i < N_THREADS; i++)
        pthread_create(&th[i], NULL, incrementer, NULL);

    for (int i = 0; i < N_THREADS; i++)
        pthread_join(th[i], NULL);

    printf("Compteur final = %ld  (attendu %d)\n",
           compteur, N_THREADS * N_ITER);

    pthread_mutex_destroy(&mtx);
    return 0;
}
/* Sans le mutex, le résultat final serait inférieur à
   N_THREADS * N_ITER (race condition sur compteur++). */
Section critique Bloc de code qui manipule une ressource partagée et doit être exécuté par au plus un thread à la fois. Le couple pthread_mutex_lock / pthread_mutex_unlock en délimite les bornes.

11. Sémaphores (alternative)

Autre mécanisme de synchronisation, plus général que le mutex (compteur).

#include <semaphore.h>

int sem_init(sem_t *semaphore, int partage, int valeur);
int sem_wait(sem_t *sem_id);   /* P : décrémente, bloque si 0 */
int sem_post(sem_t *sem_id);   /* V : incrémente, réveille un attente */
📌 À retenir
Un mutex = sémaphore binaire avec notion de propriété. Un sémaphore compte et peut gérer N ressources simultanées (producteur-consommateur, pools).

Réviser le chapitre

❓ Q/R

Pourquoi utiliser des threads plutôt que fork ?
fork est lourd : père et fils sont indépendants (espaces d'adressage et piles séparés), changement de contexte coûteux, communication inter-processus lente, partage de données délicat. Les threads partagent l'espace d'adressage ⇒ communication rapide par mémoire, création légère, context-switch facilité.
Que partagent les threads d'un même processus ? Que ne partagent-ils pas ?
Partagé : variables globales, variables statiques locales, descripteurs de fichiers, PID, PPID, propriétaires, handlers de signaux, répertoire courant, masque de fichiers.
Non partagé : identifiants de thread, pile, masque des signaux.
Signature exacte de pthread_create ?
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func)(void *), void *arg);
Renvoie 0 en cas de succès, sinon une valeur non nulle (EAGAIN, EINVAL, EPERM).
Quelle différence entre pthread_join et pthread_detach ?
pthread_join attend la fin d'un thread et récupère sa valeur de retour. pthread_detach dit au système que personne n'attendra ce thread : ses ressources sont libérées dès sa fin. Un thread détaché ne peut plus être joint.
Que se passe-t-il si le main fait return alors que d'autres threads tournent ?
Tous les threads du processus sont terminés. Pour laisser les autres continuer, on appelle pthread_exit(NULL) dans le main à la place de return / exit.
Qu'est-ce qu'un mutex ? Combien d'états ?
Un objet d'exclusion mutuelle. Deux états : libre ou verrouillé. Au plus un thread peut le tenir à un instant donné. Sert à protéger des sections critiques.
Deux manières d'initialiser un mutex ?
Statique : pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
Dynamique : pthread_mutex_init(&m, NULL); (ou avec attributs).
Libération : pthread_mutex_destroy(&m) (le mutex doit être déverrouillé).
Quel piège mène à un interblocage trivial avec un mutex ?
Verrouiller deux fois le même mutex dans le même thread sans déverrouillage entre les deux ⇒ blocage définitif du thread (le second lock attend pour toujours).
Comment compiler un programme utilisant pthread ?
gcc prog.c -o prog -lpthread et inclure <pthread.h>.
Comment voit-on les threads d'un processus avec ps ?
Avec ps aux on ne voit que le processus (les threads partagent le PID). Pour afficher les threads il faut utiliser ps maux.

🃏 Flashcards

Clique pour retourner.

Thread vs processus

1 différence clé.

tourne →
Les threads d'un processus partagent l'espace d'adressage (et le PID). Les processus créés par fork sont indépendants.

Partagé entre threads

Liste rapide.

tourne →
Globales, statiques locales, fd, PID, PPID, handlers de signaux, cwd, masque de fichiers.

NON partagé

3 choses.

tourne →
L'identifiant de thread (TID), la pile, le masque des signaux.

pthread_create

Signature.

tourne →
int pthread_create(
  pthread_t *tid,
  const pthread_attr_t *attr,
  void *(*func)(void *),
  void *arg);

Retour de pthread_create

Codes ?

tourne →
0 si succès. Sinon EAGAIN (ressources), EINVAL (attr), EPERM (permissions).

pthread_exit

Prototype.

tourne →
void pthread_exit(void *retour);

pthread_join

Rôle + signature.

tourne →
int pthread_join(pthread_t tid,
  void **retval);
Équivalent de waitpid pour les threads.

pthread_self

Que renvoie-t-il ?

tourne →
L'identificateur (pthread_t) du thread appelant.

pthread_detach

Pourquoi ?

tourne →
Libère pile + code de retour dès la fin du thread. Empêche pthread_join. Évite la saturation.

Mutex

Définition + nb d'états.

tourne →
MUTual EXclusion. 2 états : libre / verrouillé. Au plus 1 thread le tient.

Mutex statique

Initialisation.

tourne →
pthread_mutex_t m =
  PTHREAD_MUTEX_INITIALIZER;

Mutex dynamique

4 fonctions.

tourne →
pthread_mutex_init
pthread_mutex_lock
pthread_mutex_unlock
pthread_mutex_destroy

Main qui se termine

Comportement.

tourne →
Si main fait return / exittous les threads se terminent. Pour laisser tourner : pthread_exit dans le main.

Compilation

Option gcc ?

tourne →
gcc prog.c -o prog -lpthread

✎ Quiz éclair

test rapide · 7 questions
0 / 7
1.Lequel des éléments suivants n'est pas partagé entre les threads d'un même processus ?
  • Les variables globales
  • Les descripteurs de fichiers ouverts
  • La pile d'exécution
  • Le PID
Chaque thread possède sa propre pile. Identifiant et masque des signaux non plus.
2.Que renvoie pthread_create en cas de succès ?
  • Le TID du nouveau thread
  • 0
  • 1
  • L'adresse de func
Renvoie 0 en cas de succès. Le TID est rangé dans *tid.
3.Le main exécute return 0; alors que deux threads sont encore vivants. Que se passe-t-il ?
  • Le main attend les threads automatiquement
  • Le main devient un thread orphelin
  • Les threads continuent indépendamment
  • Tous les threads sont terminés avec le processus
Pour laisser tourner les autres threads, utiliser pthread_exit dans le main.
4.Quel appel est l'équivalent de waitpid pour les threads ?
  • pthread_self
  • pthread_exit
  • pthread_join
  • pthread_detach
pthread_join(tid, &retour) bloque jusqu'à la fin du thread désigné.
5.Pourquoi détacher un thread avec pthread_detach ?
  • Le rendre temps-réel
  • Lui donner sa propre pile
  • Pouvoir le joindre plus tard
  • Libérer ses ressources dès la fin et éviter la saturation
Sans detach, pile et code de retour sont conservés jusqu'à un pthread_join.
6.Combien d'états peut prendre un mutex ?
  • 2 : libre ou verrouillé
  • 3 : libre, verrouillé, détruit
  • N : compteur
  • 2 : lock partagé / lock exclusif
Un mutex est binaire avec notion de propriété. C'est un sémaphore qui compte.
7.Que produit un pthread_mutex_lock appelé deux fois de suite dans le même thread sans unlock ?
  • Aucun effet (idempotent)
  • Un segfault systématique
  • Un risque de blocage définitif du thread
  • L'incrément du compteur du mutex
Sur un mutex non récursif (cas par défaut), le second lock attend que le premier soit libéré… par lui-même.

📌 À retenir

  • Thread = processus léger ; partage l'espace d'adressage avec ses voisins.
  • Partagé : globales, statiques, fd, PID, PPID, signaux (handlers), cwd, masque fichiers.
  • Non partagé : TID, pile, masque des signaux.
  • Avantages vs fork : mémoire partagée, création légère, context-switch rapide.
  • Inconvénient : la synchronisation est à la charge du programmeur.
  • Inclure <pthread.h> · compiler avec -lpthread.
  • pthread_create(&tid, attr, func, arg) renvoie 0 si succès.
  • attr = NULL ⇒ thread joignable, ordonnancement normal.
  • pthread_exit(retour) termine le thread (à utiliser dans main pour laisser tourner les autres).
  • pthread_join(tid, &ret) attend la fin et récupère le retour (échoue si détaché).
  • pthread_detach(tid) libère les ressources dès la fin du thread.
  • pthread_self() = TID appelant · pthread_kill(tid, sig) envoie un signal.
  • Mutex : PTHREAD_MUTEX_INITIALIZER (statique) ou pthread_mutex_init (dynamique).
  • Lock / unlock / destroy — jamais deux locks de suite sans unlock (interblocage).
  • Sémaphore : <semaphore.h> · sem_init / sem_wait / sem_post.
  • Observer les threads d'un processus : ps maux (et non ps aux).