INTRODUCTION AUX THREADS POSIX (PTHREADS)

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 48
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

INTRODUCTION AUX THREADS POSIX (PTHREADS)

Post by Hydraxx »

INTRODUCTION AUX THREADS POSIX (PTHREADS)

1. Introduction générale

Un thread, ou fil d’exécution, est une unité d’exécution à l’intérieur d’un processus.

Un processus classique ne possède qu’un seul thread principal. Un processus multithreadé possède plusieurs threads qui peuvent exécuter différentes parties du même programme de manière concurrente.

Les threads POSIX sont généralement appelés Pthreads. Ils sont définis par la norme POSIX et sont très utilisés sous Linux et les systèmes de type UNIX.

Le principal intérêt des threads est de permettre à une même application d’effectuer plusieurs tâches en parallèle ou de manière concurrente, par exemple :
  • un serveur qui gère plusieurs clients ;
  • une application graphique qui continue à répondre pendant un calcul ;
  • un programme qui effectue plusieurs traitements simultanés ;
  • un logiciel qui sépare la réception réseau, le traitement et l’affichage ;
  • un moteur de jeu qui sépare le rendu, la physique, l’audio et le réseau.
Dans un système multiprocesseur ou multicœur, plusieurs threads d’un même processus peuvent réellement s’exécuter en même temps sur plusieurs cœurs.

2. Processus monothreadé et processus multithreadé

Dans un processus monothreadé, un seul flux d’exécution existe.

Dans un processus multithreadé, plusieurs flux d’exécution partagent le même espace mémoire.

Tous les threads d’un même processus :
  • exécutent le même programme ;
  • partagent les variables globales ;
  • partagent les données statiques ;
  • partagent le tas ;
  • partagent les bibliothèques chargées ;
  • partagent les descripteurs de fichiers ouverts ;
  • partagent une grande partie de l’environnement du processus.
En revanche, chaque thread possède notamment :
  • sa propre pile ;
  • son propre identifiant ;
  • son propre masque de signaux ;
  • ses propres données spécifiques au thread ;
  • sa propre valeur de errno ;
  • certains attributs d’ordonnancement ;
  • sa propre valeur de retour.
3. Organisation mémoire d’un processus multithreadé

Un processus contient généralement plusieurs zones mémoire :
  • le segment de code ;
  • les données initialisées ;
  • les données non initialisées, souvent appelées BSS ;
  • le tas ;
  • les bibliothèques partagées ;
  • une pile pour le thread principal ;
  • une pile distincte pour chaque thread supplémentaire.
Les variables globales et les objets alloués sur le tas sont accessibles à tous les threads.

Exemple :

Code: Select all

int compteur_global = 0;

void *threadFunc(void *arg)
{
    compteur_global++;
    return NULL;
}
Ici, plusieurs threads peuvent accéder à compteur_global.

Cela facilite le partage d’informations, mais crée aussi des risques de concurrence. Si plusieurs threads modifient la même donnée au même moment, le résultat peut devenir incohérent.

Ce problème sera traité avec des mécanismes de synchronisation comme :
  • les mutex ;
  • les variables de condition ;
  • les sémaphores ;
  • les verrous en lecture-écriture ;
  • les opérations atomiques.
4. Exemple d’utilisation dans un serveur

Un serveur réseau multithreadé peut fonctionner ainsi :
  1. le thread principal attend une connexion ;
  2. un client se connecte ;
  3. le serveur crée un nouveau thread ;
  4. ce nouveau thread gère le client ;
  5. le thread principal retourne immédiatement à l’attente de nouvelles connexions.
Pseudo-code :

Code: Select all

while (1)
{
    client = accepter_client();

    pthread_create(
        &thread,
        NULL,
        gerer_client,
        client
    );
}
Cette architecture permet de traiter plusieurs clients en même temps.

5. Threads et processus : principales différences

5.1 Partage des données

Les threads partagent naturellement la mémoire du processus.

Les processus séparés ont chacun leur propre espace mémoire. Pour échanger des données, ils doivent utiliser des mécanismes de communication interprocessus, par exemple :
  • pipes ;
  • sockets ;
  • files de messages ;
  • mémoire partagée ;
  • signaux ;
  • fichiers.
5.2 Coût de création

Créer un thread est généralement plus léger que créer un processus.

La création d’un processus avec fork() implique la création d’un nouveau contexte de processus et la gestion d’un nouvel espace mémoire virtuel.

Même avec la technique copy-on-write, certaines structures doivent être créées et maintenues.

La création d’un thread ne duplique pas tout l’espace mémoire du processus.

5.3 Changement de contexte

Passer d’un thread à un autre dans un même processus est souvent moins coûteux que passer d’un processus à un autre.

Les threads partagent déjà le même espace d’adressage.

5.4 Isolation

Les processus sont mieux isolés.

Une erreur mémoire dans un processus ne corrompt normalement pas directement la mémoire d’un autre processus.

Dans un processus multithreadé, une erreur dans un thread peut affecter tous les autres threads.

Exemples :
  • écriture via un pointeur invalide ;
  • corruption du tas ;
  • dépassement de pile ;
  • modification incorrecte d’une variable globale ;
  • fermeture accidentelle d’un descripteur partagé.
6. Attributs partagés entre les threads

Les threads d’un même processus partagent notamment :
  • l’identifiant du processus ;
  • l’identifiant du processus parent ;
  • l’identifiant du groupe de processus ;
  • l’identifiant de session ;
  • le terminal de contrôle ;
  • les identifiants utilisateur et groupe ;
  • les descripteurs de fichiers ouverts ;
  • les verrous d’enregistrement de fichiers ;
  • les dispositions des signaux ;
  • le répertoire de travail courant ;
  • le répertoire racine ;
  • le masque de création de fichiers ;
  • les limites de ressources ;
  • certaines temporisations ;
  • les valeurs nice et les informations générales du processus ;
  • le temps CPU global du processus.
Conséquence importante : si un thread ferme un descripteur de fichier, celui-ci n’est plus disponible pour les autres threads.

Exemple :

Code: Select all

close(fd);
Le descripteur fd étant partagé au niveau du processus, sa fermeture concerne tous les threads.

7. Attributs propres à chaque thread

Chaque thread possède notamment :
  • un identifiant de type pthread_t ;
  • une pile privée ;
  • un masque de signaux ;
  • des données spécifiques au thread ;
  • une valeur errno indépendante ;
  • une politique d’ordonnancement ;
  • une priorité d’ordonnancement ;
  • une affinité CPU ;
  • certaines capacités Linux ;
  • des informations de pile ;
  • une valeur de retour.
Même si chaque thread possède sa propre pile, toutes les piles se trouvent dans le même espace mémoire virtuel.

Techniquement, un thread peut donc accéder à la pile d’un autre thread s’il connaît son adresse. Cela est dangereux et doit être évité.

8. Inclusion de l’en-tête Pthreads

Pour utiliser les fonctions Pthreads, il faut inclure :

Code: Select all

#include <pthread.h>
9. Compilation avec Pthreads

Sous GCC ou Clang, il faut compiler et lier avec l’option :

Code: Select all

-pthread
Exemple :

Code: Select all

gcc programme.c -o programme -pthread
Il est préférable d’utiliser -pthread plutôt que seulement -lpthread.

L’option -pthread peut à la fois :
  • activer les macros nécessaires à la compilation ;
  • configurer correctement le code pour le multithreading ;
  • lier la bibliothèque Pthreads.
10. Types importants de l’API Pthreads

L’API Pthreads définit plusieurs types opaques.

Un type opaque est un type dont le programme ne doit pas supposer la représentation interne.

Principaux types :
  • pthread_t : identifiant d’un thread ;
  • pthread_attr_t : attributs de création d’un thread ;
  • pthread_mutex_t : mutex ;
  • pthread_mutexattr_t : attributs d’un mutex ;
  • pthread_cond_t : variable de condition ;
  • pthread_condattr_t : attributs d’une variable de condition ;
  • pthread_key_t : clé pour les données spécifiques à un thread ;
  • pthread_once_t : contrôle d’une initialisation exécutée une seule fois.
Il ne faut pas supposer que pthread_t est toujours un entier.

Selon l’implémentation, il peut être :
  • un entier ;
  • un entier non signé ;
  • un pointeur ;
  • une structure ;
  • un autre type interne.
11. Gestion des erreurs dans Pthreads

Les fonctions UNIX classiques retournent souvent :
  • -1 en cas d’erreur ;
  • et placent le code d’erreur dans errno.
Les fonctions Pthreads fonctionnent souvent différemment.

En général :
  • elles retournent 0 en cas de succès ;
  • elles retournent directement un numéro d’erreur positif en cas d’échec.
Exemple :

Code: Select all

int s;

s = pthread_create(&thread, NULL, threadFunc, NULL);

if (s != 0)
{
    fprintf(stderr, "pthread_create a échoué : %s\n",
            strerror(s));
}
Il ne faut donc pas systématiquement tester :

Code: Select all

if (pthread_create(...) == -1)
Ce test est incorrect pour la plupart des fonctions Pthreads.

12. errno dans un programme multithreadé

Dans un programme monothreadé ancien, errno pouvait être une simple variable globale.

Dans un programme multithreadé, cela provoquerait des conflits.

Exemple :
  1. le thread A appelle une fonction qui fixe errno ;
  2. le thread B appelle immédiatement une autre fonction ;
  3. le thread B modifie errno ;
  4. le thread A lit une valeur qui ne correspond plus à son erreur.
Pour éviter cela, chaque thread dispose d’une valeur de errno distincte.

L’implémentation utilise souvent une macro ou une fonction interne qui retourne un emplacement spécifique au thread.

Le programme peut donc utiliser normalement :

Code: Select all

if (open("fichier.txt", O_RDONLY) == -1)
{
    perror("open");
}
Chaque thread obtient sa propre valeur d’erreur.

13. Création d’un thread avec pthread_create()

Prototype :

Code: Select all

int pthread_create(
    pthread_t *thread,
    const pthread_attr_t *attr,
    void *(*start_routine)(void *),
    void *arg
);
13.1 Paramètre thread

Le paramètre thread reçoit l’identifiant du nouveau thread.

Exemple :

Code: Select all

pthread_t tid;
Puis :

Code: Select all

pthread_create(&tid, NULL, threadFunc, NULL);
13.2 Paramètre attr

Le paramètre attr permet de définir les attributs du thread.

Si sa valeur est NULL, les attributs par défaut sont utilisés.

Exemple :

Code: Select all

pthread_create(&tid, NULL, threadFunc, NULL);
13.3 Paramètre start_routine

Il s’agit de la fonction exécutée par le nouveau thread.

Cette fonction doit respecter la forme :

Code: Select all

void *fonction_thread(void *arg);
Exemple :

Code: Select all

void *threadFunc(void *arg)
{
    printf("Bonjour depuis le thread\n");
    return NULL;
}
13.4 Paramètre arg

Le paramètre arg est transmis à la fonction du thread.

Il s’agit d’un pointeur générique de type void *.

On peut transmettre :
  • un pointeur vers un entier ;
  • une chaîne de caractères ;
  • un pointeur vers une structure ;
  • un objet alloué dynamiquement.
Exemple avec une chaîne :

Code: Select all

void *threadFunc(void *arg)
{
    char *texte = arg;
    printf("%s\n", texte);
    return NULL;
}

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

    pthread_create(&tid, NULL, threadFunc, message);
    pthread_join(tid, NULL);

    return 0;
}
14. Passage de plusieurs arguments

Une fonction de thread ne reçoit qu’un seul argument de type void *.

Pour transmettre plusieurs valeurs, il faut généralement utiliser une structure.

Exemple :

Code: Select all

struct ThreadArgs
{
    int id;
    const char *nom;
    double valeur;
};

void *threadFunc(void *arg)
{
    struct ThreadArgs *args = arg;

    printf("ID : %d\n", args->id);
    printf("Nom : %s\n", args->nom);
    printf("Valeur : %f\n", args->valeur);

    return NULL;
}
Création :

Code: Select all

struct ThreadArgs args =
{
    .id = 1,
    .nom = "Worker",
    .valeur = 42.5
};

pthread_create(&tid, NULL, threadFunc, &args);
Attention : l’objet transmis doit rester valide jusqu’à ce que le thread ait fini de l’utiliser.

15. Danger lié à la durée de vie des arguments

Exemple dangereux :

Code: Select all

void creer_thread(void)
{
    pthread_t tid;
    int valeur = 42;

    pthread_create(&tid, NULL, threadFunc, &valeur);
}
La variable valeur est locale à creer_thread().

Lorsque la fonction se termine, cette variable n’est plus valide.

Le thread peut alors accéder à une adresse contenant des données invalides.

Solutions possibles :
  • attendre le thread avec pthread_join() avant de quitter la fonction ;
  • allouer les arguments dynamiquement avec malloc() ;
  • utiliser une structure ayant une durée de vie suffisante ;
  • copier immédiatement les données dans le thread.
Exemple avec allocation dynamique :

Code: Select all

int *valeur = malloc(sizeof(*valeur));

if (valeur == NULL)
{
    perror("malloc");
    exit(EXIT_FAILURE);
}

*valeur = 42;

pthread_create(&tid, NULL, threadFunc, valeur);
Dans le thread :

Code: Select all

void *threadFunc(void *arg)
{
    int valeur = *(int *)arg;
    free(arg);

    printf("%d\n", valeur);
    return NULL;
}
16. Ordonnancement après pthread_create()

Après pthread_create(), l’ordre d’exécution n’est pas garanti.

Le nouveau thread peut s’exécuter :
  • avant que pthread_create() ne retourne ;
  • juste après ;
  • plus tard ;
  • avant ou après une instruction du thread principal.
Exemple :

Code: Select all

pthread_create(&tid, NULL, threadFunc, NULL);
printf("Message du thread principal\n");
Le message du nouveau thread peut apparaître avant ou après celui du thread principal.

Il ne faut jamais dépendre de l’ordre d’exécution sans mécanisme de synchronisation.

17. Terminaison normale d’un thread

Un thread peut se terminer en retournant depuis sa fonction.

Exemple :

Code: Select all

void *threadFunc(void *arg)
{
    printf("Travail terminé\n");
    return NULL;
}
Cela est équivalent à appeler :

Code: Select all

pthread_exit(NULL);
18. Terminaison avec pthread_exit()

Prototype :

Code: Select all

void pthread_exit(void *retval);
Cette fonction termine uniquement le thread appelant.

Elle ne termine pas automatiquement tout le processus.

Exemple :

Code: Select all

void *threadFunc(void *arg)
{
    int *resultat = malloc(sizeof(*resultat));

    if (resultat == NULL)
    {
        pthread_exit(NULL);
    }

    *resultat = 123;

    pthread_exit(resultat);
}
La valeur transmise à pthread_exit() peut être récupérée par un autre thread avec pthread_join().

19. Différence entre pthread_exit() et exit()

pthread_exit() termine uniquement le thread appelant.

exit() termine le processus entier.

Donc :

Code: Select all

pthread_exit(NULL);
arrête un thread.

Alors que :

Code: Select all

exit(EXIT_SUCCESS);
arrête tous les threads du processus.

Le retour de la fonction main() équivaut également à terminer le processus entier.

20. Cas où tous les threads sont terminés

Tous les threads du processus sont terminés si :
  • la fonction main() effectue un return ;
  • un thread appelle exit() ;
  • un signal fatal termine le processus ;
  • le processus est tué ;
  • une erreur fatale provoque la terminaison générale.
Si le thread principal veut se terminer sans arrêter les autres threads, il peut appeler :

Code: Select all

pthread_exit(NULL);
Exemple :

Code: Select all

int main(void)
{
    pthread_t tid;

    pthread_create(&tid, NULL, threadFunc, NULL);

    pthread_exit(NULL);
}
Ici, le thread principal se termine, mais le processus reste actif tant qu’un autre thread continue à s’exécuter.

21. Identifiant du thread courant avec pthread_self()

Prototype :

Code: Select all

pthread_t pthread_self(void);
Cette fonction retourne l’identifiant du thread appelant.

Exemple :

Code: Select all

pthread_t moi = pthread_self();
Elle peut être utilisée pour :
  • identifier le thread courant ;
  • comparer son identifiant ;
  • se détacher soi-même ;
  • associer des informations à un thread ;
  • journaliser l’exécution.
22. Comparaison avec pthread_equal()

Prototype :

Code: Select all

int pthread_equal(pthread_t t1, pthread_t t2);
Cette fonction retourne une valeur non nulle si les deux identifiants représentent le même thread.

Exemple :

Code: Select all

if (pthread_equal(tid, pthread_self()))
{
    printf("Le thread courant est tid\n");
}
Il ne faut pas faire :

Code: Select all

if (tid == pthread_self())
Même si cela peut fonctionner sur certaines implémentations, ce n’est pas portable.

23. Identifiants Pthreads et identifiants Linux

L’identifiant retourné par pthread_self() est un identifiant Pthreads.

Il ne correspond pas nécessairement à l’identifiant de thread du noyau Linux.

Sous Linux, chaque thread possède aussi un identifiant noyau, souvent appelé TID.

Les deux notions ne doivent pas être confondues :
  • pthread_t est utilisé par l’API Pthreads ;
  • le TID Linux est utilisé par certaines interfaces spécifiques au noyau.
Pour rester portable, il faut utiliser les fonctions Pthreads avec les valeurs de type pthread_t.

24. Attente d’un thread avec pthread_join()

Prototype :

Code: Select all

int pthread_join(
    pthread_t thread,
    void **retval
);
Cette fonction attend la terminaison du thread désigné.

Si le thread est déjà terminé, elle retourne immédiatement.

Le premier paramètre est l’identifiant du thread à attendre.

Le second paramètre permet de récupérer sa valeur de retour.

Exemple simple :

Code: Select all

pthread_join(tid, NULL);
Ici, le thread appelant attend tid, sans récupérer de valeur de retour.

25. Récupération de la valeur de retour

Exemple :

Code: Select all

void *threadFunc(void *arg)
{
    int *resultat = malloc(sizeof(*resultat));

    if (resultat == NULL)
        return NULL;

    *resultat = 42;
    return resultat;
}
Dans le thread principal :

Code: Select all

void *retour;

pthread_join(tid, &retour);

if (retour != NULL)
{
    int *valeur = retour;

    printf("Résultat : %d\n", *valeur);
    free(valeur);
}
Le paramètre retval de pthread_join() reçoit la valeur retournée par :
  • le return de la fonction du thread ;
  • ou pthread_exit().
26. Ressources d’un thread terminé

Un thread joignable qui se termine conserve certaines informations jusqu’à ce qu’un autre thread appelle pthread_join().

Il peut être comparé à un thread zombie.

Le code du thread ne s’exécute plus, mais certaines ressources restent réservées.

Il faut donc :
  • soit rejoindre le thread avec pthread_join() ;
  • soit le détacher.
27. Différences entre pthread_join() et wait()

Pour les processus, wait() et waitpid() utilisent une relation parent-enfant.

Pour les threads :
  • il n’existe pas de relation parent-enfant stricte ;
  • n’importe quel thread peut rejoindre un autre thread du même processus ;
  • pthread_join() attend un thread précis ;
  • un thread ne doit normalement être rejoint qu’une fois ;
  • plusieurs threads ne doivent pas tenter de rejoindre simultanément le même thread.
28. Auto-join et blocage

Un thread ne doit pas tenter de se rejoindre lui-même.

Exemple incorrect :

Code: Select all

pthread_join(pthread_self(), NULL);
Le thread attendrait sa propre terminaison, alors qu’il ne peut pas se terminer tant que l’appel n’est pas terminé.

Cela provoque une situation de blocage ou une erreur selon l’implémentation.

Pour éviter cela :

Code: Select all

if (!pthread_equal(tid, pthread_self()))
{
    pthread_join(tid, NULL);
}
29. Exemple complet avec pthread_create() et pthread_join()

Code: Select all

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

static void *threadFunc(void *arg)
{
    const char *texte = arg;

    printf("%s\n", texte);

    return (void *)strlen(texte);
}

int main(void)
{
    pthread_t tid;
    void *retour;
    int s;

    s = pthread_create(
        &tid,
        NULL,
        threadFunc,
        "Hello world"
    );

    if (s != 0)
    {
        fprintf(stderr,
                "pthread_create : %s\n",
                strerror(s));

        exit(EXIT_FAILURE);
    }

    printf("Message from main()\n");

    s = pthread_join(tid, &retour);

    if (s != 0)
    {
        fprintf(stderr,
                "pthread_join : %s\n",
                strerror(s));

        exit(EXIT_FAILURE);
    }

    printf("Thread returned %ld\n", (long)retour);

    return EXIT_SUCCESS;
}
L’ordre entre :

Code: Select all

Message from main()
et :

Code: Select all

Hello world
peut varier.

30. Threads joignables et threads détachés

Par défaut, un thread est joignable.

Cela signifie qu’un autre thread doit appeler pthread_join() pour récupérer sa terminaison et libérer ses ressources associées.

Un thread détaché libère automatiquement ses ressources lorsqu’il se termine.

Il n’est alors plus possible de le rejoindre.

31. Détachement avec pthread_detach()

Prototype :

Code: Select all

int pthread_detach(pthread_t thread);
Exemple :

Code: Select all

pthread_detach(tid);
Après cet appel :
  • le thread devient détaché ;
  • ses ressources seront libérées automatiquement ;
  • pthread_join() ne doit plus être utilisé sur ce thread.
Un thread peut se détacher lui-même :

Code: Select all

pthread_detach(pthread_self());
Exemple :

Code: Select all

void *threadFunc(void *arg)
{
    pthread_detach(pthread_self());

    printf("Thread détaché\n");

    return NULL;
}
32. Conséquences du détachement

Une fois détaché, un thread ne peut pas redevenir joignable.

Il faut choisir :
  • thread joignable si la valeur de retour ou la confirmation de fin est nécessaire ;
  • thread détaché si personne n’a besoin de l’attendre.
Un thread détaché est adapté à des tâches autonomes, par exemple :
  • gestion indépendante d’un client réseau ;
  • journalisation ;
  • nettoyage en arrière-plan ;
  • traitement ne nécessitant aucun résultat.
33. Création directe d’un thread détaché

On peut créer un thread directement détaché grâce à un objet d’attributs.

Types et fonctions utilisées :
  • pthread_attr_t ;
  • pthread_attr_init() ;
  • pthread_attr_setdetachstate() ;
  • pthread_create() ;
  • pthread_attr_destroy().
34. Initialisation des attributs avec pthread_attr_init()

Prototype :

Code: Select all

int pthread_attr_init(pthread_attr_t *attr);
Cette fonction initialise un objet d’attributs avec les valeurs par défaut.

Exemple :

Code: Select all

pthread_attr_t attr;
int s;

s = pthread_attr_init(&attr);
L’objet doit être initialisé avant d’être utilisé.

35. Définition de l’état détaché

Prototype :

Code: Select all

int pthread_attr_setdetachstate(
    pthread_attr_t *attr,
    int detachstate
);
Valeurs principales :
  • PTHREAD_CREATE_JOINABLE ;
  • PTHREAD_CREATE_DETACHED.
Exemple :

Code: Select all

pthread_attr_setdetachstate(
    &attr,
    PTHREAD_CREATE_DETACHED
);
36. Destruction de l’objet d’attributs

Prototype :

Code: Select all

int pthread_attr_destroy(pthread_attr_t *attr);
Cette fonction détruit l’objet d’attributs.

Exemple :

Code: Select all

pthread_attr_destroy(&attr);
Elle ne détruit pas le thread créé.

Elle libère uniquement les ressources internes associées à l’objet pthread_attr_t.

37. Exemple complet de thread créé détaché

Code: Select all

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

static void *threadFunc(void *arg)
{
    printf("Thread détaché en cours\n");
    return NULL;
}

int main(void)
{
    pthread_t tid;
    pthread_attr_t attr;
    int s;

    s = pthread_attr_init(&attr);

    if (s != 0)
    {
        fprintf(stderr,
                "pthread_attr_init : %s\n",
                strerror(s));

        exit(EXIT_FAILURE);
    }

    s = pthread_attr_setdetachstate(
        &attr,
        PTHREAD_CREATE_DETACHED
    );

    if (s != 0)
    {
        fprintf(stderr,
                "pthread_attr_setdetachstate : %s\n",
                strerror(s));

        exit(EXIT_FAILURE);
    }

    s = pthread_create(
        &tid,
        &attr,
        threadFunc,
        NULL
    );

    if (s != 0)
    {
        fprintf(stderr,
                "pthread_create : %s\n",
                strerror(s));

        exit(EXIT_FAILURE);
    }

    s = pthread_attr_destroy(&attr);

    if (s != 0)
    {
        fprintf(stderr,
                "pthread_attr_destroy : %s\n",
                strerror(s));

        exit(EXIT_FAILURE);
    }

    pthread_exit(NULL);
}
38. Choisir entre thread joignable et détaché

Utiliser un thread joignable lorsque :
  • on veut connaître sa fin ;
  • on veut récupérer une valeur de retour ;
  • on veut synchroniser le programme ;
  • on doit attendre que son travail soit terminé ;
  • on doit garantir la libération d’une ressource après sa fin.
Utiliser un thread détaché lorsque :
  • le résultat n’est pas nécessaire ;
  • aucun thread n’a besoin de l’attendre ;
  • la tâche est complètement autonome ;
  • on veut une récupération automatique des ressources.
39. Risques particuliers des threads

Le multithreading introduit plusieurs catégories de problèmes.

39.1 Conditions de course

Une condition de course apparaît lorsque le résultat dépend de l’ordre imprévisible d’exécution des threads.

Exemple :

Code: Select all

compteur++;
Cette instruction peut être décomposée en plusieurs étapes :
  1. lecture du compteur ;
  2. incrémentation ;
  3. écriture du résultat.
Deux threads peuvent lire la même ancienne valeur et perdre une incrémentation.

39.2 Interblocages

Un interblocage se produit lorsque plusieurs threads s’attendent mutuellement.

Exemple :
  • le thread A possède le verrou 1 et attend le verrou 2 ;
  • le thread B possède le verrou 2 et attend le verrou 1.
39.3 Famine

Un thread peut ne jamais obtenir les ressources nécessaires parce que d’autres threads sont toujours favorisés.

39.4 Corruption de mémoire

Tous les threads partagent la mémoire.

Une erreur dans un seul thread peut détruire les données de tous les autres.

39.5 Ordre d’exécution imprévisible

Le programme ne doit pas supposer qu’un thread commencera ou terminera avant un autre sans synchronisation explicite.

40. Threads et signaux

La gestion des signaux devient plus complexe dans une application multithreadée.

Certains éléments sont partagés au niveau du processus, tandis que le masque de signaux est propre à chaque thread.

En pratique, il est souvent recommandé de :
  • bloquer certains signaux dans les threads ;
  • désigner un thread spécifique pour les traiter ;
  • utiliser des fonctions adaptées aux programmes multithreadés.
41. Limitation liée aux piles

Chaque thread possède sa propre pile.

Créer un grand nombre de threads peut donc consommer beaucoup d’espace mémoire virtuel.

Sur un système 32 bits, l’espace d’adressage est limité.

Même si la mémoire physique n’est pas entièrement utilisée, les réservations de piles peuvent empêcher la création de nouveaux threads.

Il faut éviter de créer un nombre illimité de threads.

Une architecture avec pool de threads est souvent préférable.

42. Tableau récapitulatif des API principales

42. Tableau récapitulatif des API principales
  • pthread_create() : crée un nouveau thread.
  • pthread_exit() : termine uniquement le thread appelant.
  • pthread_self() : retourne l’identifiant du thread courant.
  • pthread_equal() : compare deux identifiants de threads.
  • pthread_join() : attend la fin d’un thread joignable et récupère ses ressources.
  • pthread_detach() : détache un thread afin que ses ressources soient libérées automatiquement à sa terminaison.
  • pthread_attr_init() : initialise un objet contenant les attributs de création d’un thread.
  • pthread_attr_setdetachstate() : définit si le thread sera joignable ou détaché.
  • pthread_attr_destroy() : détruit l’objet d’attributs après son utilisation.
43. Prototypes essentiels à connaître

Code: Select all

#include <pthread.h>

int pthread_create(
    pthread_t *thread,
    const pthread_attr_t *attr,
    void *(*start_routine)(void *),
    void *arg
);

void pthread_exit(void *retval);

pthread_t pthread_self(void);

int pthread_equal(
    pthread_t t1,
    pthread_t t2
);

int pthread_join(
    pthread_t thread,
    void **retval
);

int pthread_detach(
    pthread_t thread
);

int pthread_attr_init(
    pthread_attr_t *attr
);

int pthread_attr_setdetachstate(
    pthread_attr_t *attr,
    int detachstate
);

int pthread_attr_destroy(
    pthread_attr_t *attr
);
44. Bonnes pratiques
  • Toujours vérifier les valeurs de retour des fonctions Pthreads.
  • Utiliser strerror(code_erreur) avec les erreurs Pthreads.
  • Compiler avec -pthread.
  • Ne pas comparer directement deux pthread_t avec ==.
  • Utiliser pthread_equal().
  • Ne pas transmettre l’adresse d’une variable locale qui disparaîtra trop tôt.
  • Choisir clairement entre thread joignable et thread détaché.
  • Toujours joindre ou détacher les threads.
  • Ne pas tenter de joindre un thread détaché.
  • Ne pas joindre deux fois le même thread.
  • Ne pas appeler pthread_join() sur le thread courant.
  • Ne pas supposer l’ordre d’exécution des threads.
  • Protéger les données partagées avec des mécanismes de synchronisation.
  • Éviter de créer un nombre illimité de threads.
  • Faire attention aux descripteurs de fichiers partagés.
  • Faire attention à la durée de vie des valeurs de retour.
45. Exemple final complet et robuste

Code: Select all

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

struct ThreadArgs
{
    int id;
    const char *message;
};

struct ThreadResult
{
    int id;
    size_t longueur;
};

static void *threadFunc(void *arg)
{
    struct ThreadArgs *args = arg;

    struct ThreadResult *result =
        malloc(sizeof(*result));

    if (result == NULL)
    {
        return NULL;
    }

    printf(
        "Thread %d : %s\n",
        args->id,
        args->message
    );

    result->id = args->id;
    result->longueur = strlen(args->message);

    return result;
}

int main(void)
{
    pthread_t tid;
    struct ThreadArgs args;
    struct ThreadResult *result;
    void *retour;
    int s;

    args.id = 1;
    args.message = "Bonjour depuis Pthreads";

    s = pthread_create(
        &tid,
        NULL,
        threadFunc,
        &args
    );

    if (s != 0)
    {
        fprintf(
            stderr,
            "pthread_create : %s\n",
            strerror(s)
        );

        return EXIT_FAILURE;
    }

    s = pthread_join(tid, &retour);

    if (s != 0)
    {
        fprintf(
            stderr,
            "pthread_join : %s\n",
            strerror(s)
        );

        return EXIT_FAILURE;
    }

    result = retour;

    if (result != NULL)
    {
        printf(
            "Résultat du thread %d : longueur = %zu\n",
            result->id,
            result->longueur
        );

        free(result);
    }

    return EXIT_SUCCESS;
}
Compilation :

Code: Select all

gcc programme.c -o programme -pthread
46. Résumé final

Les threads POSIX permettent à un processus d’exécuter plusieurs tâches de manière concurrente.

Ils partagent l’espace mémoire, les variables globales, le tas et de nombreux attributs du processus, mais chaque thread possède sa propre pile et certains attributs individuels.

La création se fait avec :

Code: Select all

pthread_create()
La terminaison peut se faire par :

Code: Select all

return
ou :

Code: Select all

pthread_exit()
Un thread joignable doit être attendu avec :

Code: Select all

pthread_join()
Un thread détaché libère automatiquement ses ressources et peut être créé ou rendu détaché avec :

Code: Select all

pthread_detach()
ou avec les attributs :

Code: Select all

pthread_attr_init()
pthread_attr_setdetachstate()
pthread_attr_destroy()
Les threads sont généralement plus légers que les processus et facilitent le partage des données, mais ils demandent une grande rigueur.

Une mauvaise synchronisation, une erreur mémoire ou une mauvaise gestion de la durée de vie des données peut affecter tout le processus.

La maîtrise des threads repose donc sur trois principes fondamentaux :
  1. bien gérer leur cycle de vie ;
  2. bien gérer les données partagées ;
  3. ne jamais supposer un ordre d’exécution sans synchronisation.

Who is online

Users browsing this forum: No registered users and 0 guests