THREAD SAFETY ET STOCKAGE PROPRE À CHAQUE THREAD

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

Moderator: Rick

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

THREAD SAFETY ET STOCKAGE PROPRE À CHAQUE THREAD

Post by Hydraxx »

THREAD SAFETY ET STOCKAGE PROPRE À CHAQUE THREAD
POSIX Threads — pthread_once, données spécifiques aux threads et stockage local

1. OBJECTIF DU CHAPITRE

Dans un programme multithread, plusieurs threads peuvent appeler simultanément la même fonction. Cette situation devient dangereuse dès que la fonction utilise un état partagé : variable globale, variable static, tampon commun, liste globale ou ressource modifiable.

Ce cours présente trois moyens de résoudre le problème :

1. protéger les données partagées avec un mutex ;
2. écrire une fonction réentrante ne dépendant d’aucun état partagé ;
3. donner à chaque thread sa propre copie des données avec les Thread-Specific Data ou le stockage local au thread.

Nous verrons également comment exécuter une initialisation exactement une fois avec pthread_once().

2. THREAD-SAFE, RÉENTRANTE ET NON RÉENTRANTE

Fonction thread-safe

Une fonction est thread-safe lorsqu’elle peut être appelée simultanément par plusieurs threads sans course critique, corruption mémoire ni résultat incohérent.

Elle peut utiliser un état partagé à condition que cet état soit correctement synchronisé.

Fonction réentrante

Une fonction est réentrante lorsqu’une nouvelle invocation peut commencer avant la fin d’une invocation précédente sans perturber celle-ci.

Une fonction réentrante évite normalement :
  • les variables globales modifiables ;
  • les variables static modifiables ;
  • les tampons partagés ;
  • un état caché conservé entre les appels ;
  • les pointeurs vers une zone interne réutilisée.
Les données nécessaires sont placées dans des variables locales ou dans un tampon fourni par l’appelant.

Relation importante :

Une fonction réentrante est normalement thread-safe. En revanche, une fonction thread-safe protégée par un mutex n’est pas forcément réentrante.

Un mutex protège les appels concurrents entre threads, mais une fonction interrompue alors qu’elle détient le mutex pourrait se bloquer si elle est réappelée dans un contexte comme un gestionnaire de signal.

3. EXEMPLE DE COURSE CRITIQUE

Le programme suivant incrémente une variable globale sans synchronisation :

Code: Select all

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

static int g_value = 0;

static void *worker(void *arg)
{
    int loops = *(int *)arg;

    for (int i = 0; i < loops; i++) {
        int local = g_value;
        local++;
        g_value = local;
    }

    return NULL;
}

int main(void)
{
    pthread_t t1;
    pthread_t t2;
    int loops = 100000;

    pthread_create(&t1, NULL, worker, &loops);
    pthread_create(&t2, NULL, worker, &loops);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("Résultat : %d\n", g_value);
    return 0;
}
Le résultat attendu est 200000, mais il peut être inférieur.

Séquence possible :

Code: Select all

Thread A lit g_value = 42
Thread B lit g_value = 42
Thread A écrit 43
Thread B écrit 43
Deux incréments ont eu lieu, mais la valeur n’a progressé que d’une unité.

Attention : le mot-clé volatile ne rend pas cette opération atomique et ne remplace pas un mutex.

4. PREMIÈRE SOLUTION : LE MUTEX

Code: Select all

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

static int g_value = 0;
static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;

static void *worker(void *arg)
{
    int loops = *(int *)arg;

    for (int i = 0; i < loops; i++) {
        pthread_mutex_lock(&g_mutex);
        g_value++;
        pthread_mutex_unlock(&g_mutex);
    }

    return NULL;
}
La fonction devient thread-safe, car un seul thread modifie g_value à la fois.

Bonne pratique : verrouiller uniquement la section qui accède aux données partagées. Si le mutex couvre toute une fonction longue, les appels sont totalement sérialisés et le parallélisme disparaît.

Un mutex est adapté lorsque les threads doivent réellement partager la même donnée. Il n’est pas nécessaire lorsque chaque thread peut travailler sur sa propre copie.

5. PRÉFÉRER UNE INTERFACE RÉENTRANTE

Une mauvaise interface renvoie un pointeur vers un tampon interne :

Code: Select all

char *format_value(int value);
Une interface réentrante demande à l’appelant de fournir le tampon :

Code: Select all

int format_value_r(int value, char *buffer, size_t size);
Exemple :

Code: Select all

#include <stdio.h>
#include <stddef.h>

int format_value_r(int value, char *buffer, size_t size)
{
    if (buffer == NULL || size == 0)
        return -1;

    int written = snprintf(buffer, size, "Valeur = %d", value);

    if (written < 0 || (size_t)written >= size)
        return -1;

    return 0;
}
Chaque thread peut utiliser son propre tableau local :

Code: Select all

char buffer[64];

if (format_value_r(42, buffer, sizeof(buffer)) == 0)
    printf("%s\n", buffer);
Aucun mutex n’est nécessaire, car le stockage appartient à l’appelant.

Principe : lorsque l’interface peut être modifiée, le tampon fourni par l’appelant est souvent la solution la plus propre.

6. FONCTIONS HISTORIQUES NON THREAD-SAFE

Certaines anciennes interfaces renvoient un pointeur vers un stockage interne statique ou maintiennent un état global.

Exemples classiques :
  • asctime() et ctime() peuvent utiliser un tampon statique ;
  • gmtime() et localtime() ont historiquement renvoyé une structure interne ;
  • strtok() maintient un état caché entre plusieurs appels ;
  • d’anciennes fonctions de consultation des utilisateurs, groupes, hôtes ou réseaux possèdent des variantes réentrantes ;
  • certaines fonctions historiques portent une variante dont le nom se termine par _r.
Les variantes réentrantes reçoivent généralement un tampon et sa taille.

Portabilité : toutes les variantes _r ne sont pas disponibles ou normalisées de la même manière sur tous les systèmes. Il faut consulter la documentation de la plateforme ciblée.

7. INITIALISATION UNIQUE AVEC pthread_once()

Dans une bibliothèque, l’initialisation ne peut pas toujours être placée dans main(). Le premier appel peut provenir de n’importe quel thread.

Un simple booléen n’est pas suffisant :

Code: Select all

if (!initialized) {
    initialize();
    initialized = 1;
}
Deux threads peuvent observer simultanément initialized == 0 et exécuter deux fois l’initialisation.

La solution POSIX est pthread_once().

Prototype

Code: Select all

#include <pthread.h>

int pthread_once(pthread_once_t *once_control,
                 void (*init)(void));
La variable de contrôle doit être initialisée avec PTHREAD_ONCE_INIT :

Code: Select all

static pthread_once_t g_once = PTHREAD_ONCE_INIT;
La fonction d’initialisation ne reçoit aucun argument et ne renvoie rien :

Code: Select all

static void initialize_library(void)
{
    /* Initialisation exécutée une seule fois */
}
Tous les threads peuvent ensuite appeler :

Code: Select all

int status = pthread_once(&g_once, initialize_library);
Pthreads garantit que initialize_library() est exécutée une seule fois dans tout le processus.

Exemple complet

Code: Select all

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

static pthread_once_t g_once = PTHREAD_ONCE_INIT;
static int *g_table = NULL;

static void initialize_table(void)
{
    g_table = malloc(256 * sizeof(*g_table));

    if (g_table == NULL) {
        fputs("Allocation impossible\n", stderr);
        abort();
    }

    for (int i = 0; i < 256; i++)
        g_table[i] = i * i;
}

static void *worker(void *arg)
{
    (void)arg;

    int status = pthread_once(&g_once, initialize_table);

    if (status != 0)
        return NULL;

    printf("g_table[10] = %d\n", g_table[10]);
    return NULL;
}
Utilisations typiques :
  • création d’une clé de données spécifiques aux threads ;
  • initialisation paresseuse d’une bibliothèque ;
  • construction d’une table globale en lecture seule ;
  • initialisation d’un cache ;
  • chargement unique d’une configuration.
8. DONNÉES SPÉCIFIQUES AUX THREADS

Les Thread-Specific Data, abrégées TSD, permettent de conserver une valeur persistante différente pour chaque thread.

Une clé est commune au processus, mais la valeur associée dépend du thread :

Code: Select all

Clé K
 ├── thread A -> pointeur vers le contexte A
 ├── thread B -> pointeur vers le contexte B
 └── thread C -> pointeur vers le contexte C
Le même appel :

Code: Select all

pthread_getspecific(K);
peut donc renvoyer trois adresses différentes selon le thread qui l’exécute.

Contrairement à une variable automatique, la donnée survit au retour de la fonction et peut être récupérée lors de l’appel suivant effectué par le même thread.

9. pthread_key_create()

Prototype

Code: Select all

#include <pthread.h>

int pthread_key_create(pthread_key_t *key,
                       void (*destructor)(void *));
Cette fonction crée une clé globale au processus.

key reçoit l’identifiant de la clé.

destructor est une fonction facultative appelée lorsqu’un thread se termine alors qu’une valeur non NULL est encore associée à cette clé.

Forme du destructeur :

Code: Select all

static void destroy_value(void *value)
{
    free(value);
}
Création :

Code: Select all

static pthread_key_t g_key;

int status = pthread_key_create(&g_key, destroy_value);
Points essentiels :
  • la clé n’est créée qu’une seule fois ;
  • tous les threads utilisent le même identifiant de clé ;
  • chaque thread possède sa propre valeur associée ;
  • le destructeur reçoit la valeur du thread qui se termine ;
  • le destructeur peut être NULL ;
  • l’ordre d’appel de plusieurs destructeurs n’est pas garanti.
La combinaison normale est :

Code: Select all

static pthread_once_t g_once = PTHREAD_ONCE_INIT;
static pthread_key_t g_key;

static void create_key(void)
{
    int status = pthread_key_create(&g_key, free);

    if (status != 0)
        abort();
}
10. pthread_setspecific()

Prototype

Code: Select all

int pthread_setspecific(pthread_key_t key,
                        const void *value);
Cette fonction associe value à key pour le thread appelant uniquement.

Elle ne copie pas le contenu du bloc : elle mémorise seulement le pointeur.

Code: Select all

struct thread_context *ctx = malloc(sizeof(*ctx));

if (ctx == NULL)
    return NULL;

int status = pthread_setspecific(g_key, ctx);
Un autre thread peut utiliser la même clé tout en enregistrant une autre adresse.

Pour supprimer l’association du thread courant :

Code: Select all

pthread_setspecific(g_key, NULL);
Attention : si une ancienne valeur pointait vers une allocation dynamique, la remplacer ne la libère pas automatiquement. Il faut d’abord récupérer et libérer l’ancien objet lorsque cela est nécessaire.

11. pthread_getspecific()

Prototype

Code: Select all

void *pthread_getspecific(pthread_key_t key);
La fonction renvoie la valeur associée à la clé pour le thread courant.

Si aucune valeur n’a encore été enregistrée, elle renvoie NULL.

Cette propriété permet une allocation paresseuse :

Code: Select all

struct thread_context *ctx = pthread_getspecific(g_key);

if (ctx == NULL) {
    ctx = calloc(1, sizeof(*ctx));

    if (ctx == NULL)
        return NULL;

    int status = pthread_setspecific(g_key, ctx);

    if (status != 0) {
        free(ctx);
        return NULL;
    }
}
Lors des appels suivants exécutés par le même thread, pthread_getspecific() retrouve le même contexte.

12. pthread_key_delete()

Prototype

Code: Select all

int pthread_key_delete(pthread_key_t key);
Cette fonction supprime une clé.

Très important : pthread_key_delete() n’appelle pas les destructeurs des valeurs actuellement associées aux threads. Elle ne libère donc pas automatiquement les objets pointés.

Une clé de bibliothèque utilisée jusqu’à la fin du processus est souvent laissée active pendant toute la vie du programme.

Il ne faut pas supprimer une clé pendant que d’autres threads peuvent encore l’utiliser.

13. EXEMPLE COMPLET DE CONTEXTE PAR THREAD

Code: Select all

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

struct thread_context {
    unsigned long calls;
    char buffer[128];
};

static pthread_once_t g_once = PTHREAD_ONCE_INIT;
static pthread_key_t g_context_key;

static void destroy_context(void *value)
{
    struct thread_context *ctx = value;
    free(ctx);
}

static void create_context_key(void)
{
    int status = pthread_key_create(&g_context_key,
                                    destroy_context);

    if (status != 0) {
        fprintf(stderr,
                "pthread_key_create a échoué : %d\n",
                status);
        abort();
    }
}

static struct thread_context *get_context(void)
{
    int status = pthread_once(&g_once, create_context_key);

    if (status != 0)
        return NULL;

    struct thread_context *ctx =
        pthread_getspecific(g_context_key);

    if (ctx != NULL)
        return ctx;

    ctx = calloc(1, sizeof(*ctx));

    if (ctx == NULL)
        return NULL;

    status = pthread_setspecific(g_context_key, ctx);

    if (status != 0) {
        free(ctx);
        return NULL;
    }

    return ctx;
}

static const char *format_for_current_thread(int value)
{
    struct thread_context *ctx = get_context();

    if (ctx == NULL)
        return NULL;

    ctx->calls++;

    snprintf(ctx->buffer,
             sizeof(ctx->buffer),
             "thread=%lu, appel=%lu, valeur=%d",
             (unsigned long)pthread_self(),
             ctx->calls,
             value);

    return ctx->buffer;
}

static void *worker(void *arg)
{
    int id = *(int *)arg;

    for (int i = 0; i < 3; i++) {
        const char *text =
            format_for_current_thread(id * 100 + i);

        if (text != NULL)
            puts(text);
    }

    return NULL;
}

int main(void)
{
    pthread_t threads[3];
    int ids[3] = { 1, 2, 3 };

    for (int i = 0; i < 3; i++) {
        int status = pthread_create(&threads[i],
                                    NULL,
                                    worker,
                                    &ids[i]);

        if (status != 0) {
            fprintf(stderr,
                    "pthread_create a échoué : %d\n",
                    status);
            return EXIT_FAILURE;
        }
    }

    for (int i = 0; i < 3; i++) {
        int status = pthread_join(threads[i], NULL);

        if (status != 0) {
            fprintf(stderr,
                    "pthread_join a échoué : %d\n",
                    status);
            return EXIT_FAILURE;
        }
    }

    return EXIT_SUCCESS;
}
Chaque thread possède :
  • son compteur calls ;
  • son propre tampon buffer ;
  • une allocation libérée automatiquement à la fin du thread.
14. COMMENT LES TSD PEUVENT ÊTRE IMPLÉMENTÉES

Une implémentation peut maintenir deux niveaux de tableaux.

Table globale des clés

Elle appartient au processus et peut contenir, pour chaque clé :
  • un indicateur précisant si la clé est utilisée ;
  • l’adresse du destructeur associé.
Table locale de chaque thread

Chaque thread possède un tableau de pointeurs :

Code: Select all

TSD du thread A :
index 0 -> NULL
index 1 -> contexte A
index 2 -> autre objet A

TSD du thread B :
index 0 -> NULL
index 1 -> contexte B
index 2 -> autre objet B
La même clé numérique sert d’index dans chaque tableau, mais les valeurs sont différentes.

pthread_setspecific() écrit dans le tableau du thread courant.

pthread_getspecific() lit dans le tableau du thread courant.

Cette représentation explique pourquoi une clé est globale tandis que sa valeur est privée.

15. DESTRUCTEURS ET FIN DE THREAD

Lorsqu’un thread se termine normalement, Pthreads examine ses valeurs spécifiques.

Si une valeur non NULL possède un destructeur, le destructeur est appelé :

Code: Select all

static void destroy_context(void *value)
{
    free(value);
}
Le mécanisme est particulièrement utile lorsqu’un thread quitte par :
  • un retour depuis sa fonction de démarrage ;
  • un appel à pthread_exit() ;
  • la fin normale du traitement.
Règles de conception :
  • un destructeur ne doit pas dépendre de l’ordre d’exécution des autres destructeurs ;
  • il doit accepter la valeur exacte enregistrée avec pthread_setspecific() ;
  • il doit tolérer un objet partiellement initialisé si le code peut échouer en cours de construction ;
  • il ne doit pas supposer que les autres contextes existent encore.
Erreur fréquente : utiliser un pointeur vers une variable locale comme valeur spécifique. La variable disparaît au retour de la fonction, tandis que le pointeur reste enregistré.

16. LIMITES DES CLÉS

Le nombre de clés disponibles n’est pas infini.

La constante suivante peut indiquer une limite minimale ou connue à la compilation :

Code: Select all

PTHREAD_KEYS_MAX
Une interrogation à l’exécution peut être disponible :

Code: Select all

#include <unistd.h>

long max_keys = sysconf(_SC_THREAD_KEYS_MAX);
Bonne architecture : une bibliothèque devrait généralement créer une seule clé pointant vers une structure regroupant toutes ses données privées, plutôt qu’une clé pour chaque petite variable.

Code: Select all

struct library_tls {
    char error_buffer[256];
    unsigned long operation_count;
    void *temporary_object;
};
17. PROBLÈME DU TAMPON STATIQUE DE strerror()

Une implémentation simplifiée non thread-safe peut utiliser :

Code: Select all

static char g_buffer[256];

char *my_strerror(int error)
{
    snprintf(g_buffer,
             sizeof(g_buffer),
             "Erreur numéro %d",
             error);

    return g_buffer;
}
Scénario :

Code: Select all

Thread principal :
    p1 = my_strerror(1);

Autre thread :
    p2 = my_strerror(2);

Thread principal :
    affiche p1
p1 et p2 pointent vers le même tableau. Le second appel a écrasé le texte du premier.

Un mutex autour de my_strerror() ne suffit pas toujours : le mutex serait relâché avant que l’appelant ait fini d’utiliser le pointeur retourné.

La donnée doit donc être propre au thread, ou le tampon doit être fourni par l’appelant.

18. VERSION THREAD-SAFE AVEC pthread_once ET TSD

Code: Select all

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

#define ERROR_BUFFER_SIZE 256

static pthread_once_t g_once = PTHREAD_ONCE_INIT;
static pthread_key_t g_error_key;

static void destroy_error_buffer(void *value)
{
    free(value);
}

static void create_error_key(void)
{
    int status = pthread_key_create(&g_error_key,
                                    destroy_error_buffer);

    if (status != 0)
        abort();
}

char *my_strerror_tsd(int error)
{
    int status = pthread_once(&g_once, create_error_key);

    if (status != 0)
        return NULL;

    char *buffer = pthread_getspecific(g_error_key);

    if (buffer == NULL) {
        buffer = malloc(ERROR_BUFFER_SIZE);

        if (buffer == NULL)
            return NULL;

        status = pthread_setspecific(g_error_key, buffer);

        if (status != 0) {
            free(buffer);
            return NULL;
        }
    }

    snprintf(buffer,
             ERROR_BUFFER_SIZE,
             "Erreur numéro %d",
             error);

    return buffer;
}
Déroulement du premier appel d’un thread :

Code: Select all

pthread_once()
    -> création globale de la clé si nécessaire

pthread_getspecific()
    -> NULL, car ce thread n’a pas encore de tampon

malloc()
    -> allocation du tampon du thread

pthread_setspecific()
    -> mémorisation du pointeur pour ce thread
Déroulement des appels suivants du même thread :

Code: Select all

pthread_getspecific()
    -> renvoie directement le tampon déjà alloué
À la fin du thread :

Code: Select all

destroy_error_buffer()
    -> free(buffer)
Chaque thread obtient une adresse différente. L’interface de la fonction n’a pas été modifiée.

19. STOCKAGE LOCAL AU THREAD AVEC __thread

GCC et Clang fournissent le spécificateur :

Code: Select all

__thread
Exemple :

Code: Select all

static __thread char g_error_buffer[256];
Chaque thread possède automatiquement sa propre instance du tableau.

La fonction précédente devient beaucoup plus simple :

Code: Select all

#include <stdio.h>

#define ERROR_BUFFER_SIZE 256

char *my_strerror_tls(int error)
{
    static __thread char buffer[ERROR_BUFFER_SIZE];

    snprintf(buffer,
             sizeof(buffer),
             "Erreur numéro %d",
             error);

    return buffer;
}
Il n’y a plus besoin de :
  • pthread_once() ;
  • pthread_key_create() ;
  • pthread_getspecific() ;
  • pthread_setspecific() ;
  • malloc() ;
  • destructeur.
Durée de vie

Chaque instance existe pendant la vie du thread.

Adresse

L’opérateur & peut être utilisé, mais l’adresse obtenue désigne l’instance du thread courant.

Support nécessaire

Le compilateur, l’éditeur de liens, le chargeur et le système doivent prendre en charge le stockage local au thread.

20. ÉQUIVALENTS MODERNES

En C11, le mot-clé standard est :

Code: Select all

_Thread_local
Exemple :

Code: Select all

static _Thread_local char buffer[256];
Avec l’en-tête standard :

Code: Select all

#include <threads.h>

static thread_local char buffer[256];
Selon l’implémentation, thread_local peut être défini comme un alias de _Thread_local.

En C++11 et versions ultérieures :

Code: Select all

static thread_local char buffer[256];
Choix conseillé :
  • utiliser _Thread_local pour du C standard moderne ;
  • utiliser thread_local en C++ ;
  • utiliser __thread lorsque l’on cible explicitement GCC ou Clang et que les contraintes de la plateforme sont connues ;
  • utiliser les TSD Pthreads pour des données dynamiques, des destructeurs configurables ou une bibliothèque POSIX devant rester très portable.
21. TSD OU TLS DU COMPILATEUR ?

TSD Pthreads

Avantages :
  • API POSIX ;
  • valeur dynamique de type pointeur ;
  • destructeur associé à la clé ;
  • adaptée aux structures complexes ;
  • allocation paresseuse possible ;
  • l’interface publique d’une fonction peut rester inchangée.
Inconvénients :
  • plus de code ;
  • allocation éventuelle ;
  • accès indirect par une clé ;
  • gestion des erreurs ;
  • nombre de clés limité.
Stockage local au thread du compilateur

Avantages :
  • syntaxe très simple ;
  • accès direct à la variable ;
  • aucune table de clés visible dans le code ;
  • aucune allocation pour un tableau de taille fixe ;
  • souvent très efficace.
Inconvénients :
  • contraintes de plateforme et de modèle de chargement ;
  • moins flexible pour des objets créés dynamiquement ;
  • initialisation et destruction plus délicates selon le langage ;
  • __thread n’est pas du C ISO.
22. ERREURS FRÉQUENTES

1. Créer la clé à chaque appel

Code: Select all

pthread_key_create(&key, free);
ne doit pas être exécuté à chaque invocation de la fonction. La clé doit être créée une seule fois avec pthread_once().

2. Enregistrer l’adresse d’une variable locale

Code: Select all

int local;
pthread_setspecific(key, &local);
Après le retour de la fonction, le pointeur devient invalide.

3. Oublier de libérer après un échec de pthread_setspecific()

Code: Select all

ptr = malloc(size);

if (pthread_setspecific(key, ptr) != 0) {
    free(ptr);
    return NULL;
}
4. Croire que pthread_setspecific() copie l’objet

La fonction copie uniquement la valeur du pointeur.

5. Remplacer une valeur sans libérer l’ancienne

Pthreads n’appelle pas automatiquement le destructeur au moment du remplacement.

6. Supprimer la clé trop tôt

Il est dangereux d’appeler pthread_key_delete() pendant que d’autres threads peuvent encore utiliser la clé.

7. Utiliser uniquement un mutex autour d’un tampon retourné

Si la fonction renvoie l’adresse du tampon puis déverrouille le mutex, un autre thread peut écraser le tampon avant son utilisation par l’appelant.

8. Supposer un ordre entre les destructeurs

L’ordre de nettoyage de plusieurs clés n’est pas garanti.

23. COMPILATION

Commande recommandée :

Code: Select all

gcc -std=c11 -Wall -Wextra -Wpedantic -pthread programme.c -o programme
-pthread configure à la fois la compilation et l’édition de liens pour Pthreads.

Pour utiliser explicitement __thread, GCC ou Clang l’acceptent comme extension. Avec -Wpedantic, un avertissement peut rappeler que ce mot-clé n’appartient pas au C ISO.

24. TABLEAU RÉCAPITULATIF DES API

Élément — Rôle

pthread_once_t : Type de la variable contrôlant une initialisation unique

PTHREAD_ONCE_INIT : Initialisation statique d’un pthread_once_t

pthread_once() : Exécute une fonction d’initialisation une seule fois

pthread_key_t : Type représentant une clé de données spécifiques aux threads

pthread_key_create() : Crée une clé et associe éventuellement un destructeur

pthread_setspecific() : Associe une valeur à la clé pour le thread courant

pthread_getspecific() : Récupère la valeur du thread courant

pthread_key_delete() : Supprime la clé sans appeler les destructeurs des valeurs existantes

PTHREAD_KEYS_MAX : Limite liée au nombre de clés disponibles

sysconf(_SC_THREAD_KEYS_MAX) : Interroge la limite à l’exécution lorsque disponible

__thread : Extension GNU créant une variable distincte pour chaque thread

_Thread_local : Spécificateur standard C11 de stockage local au thread

thread_local : Spécificateur C++ et macro possible de threads.h en C

25. MÉTHODE DE CHOIX

Posez les questions suivantes :

La donnée doit-elle être réellement partagée ?

Oui : utiliser un mutex, un verrou lecture-écriture ou une primitive atomique adaptée.

L’interface peut-elle recevoir un tampon ?

Oui : préférer une fonction réentrante avec stockage fourni par l’appelant.

L’interface doit-elle rester inchangée ?

Oui : utiliser une donnée spécifique au thread.

La donnée est-elle un simple objet déclaré statiquement ?

Oui : envisager _Thread_local, thread_local ou __thread.

La donnée est-elle dynamique et doit-elle avoir un destructeur ?

Oui : utiliser pthread_key_create() avec un destructeur.

26. CONCLUSION

La meilleure fonction multithread est souvent celle qui ne partage aucun état modifiable.

Lorsque cela est possible, une interface réentrante avec un tampon fourni par l’appelant est simple, portable et efficace.

Lorsque l’interface ne peut pas être changée, les données spécifiques aux threads permettent de conserver un état privé et persistant pour chaque thread :

Code: Select all

pthread_once()
pthread_key_create()
pthread_getspecific()
pthread_setspecific()
Le destructeur associé à la clé automatise le nettoyage à la fin du thread.

Pour une variable simple, le stockage local au thread offre une solution encore plus directe :

Code: Select all

static _Thread_local char buffer[256];
Le point essentiel est de distinguer clairement :
  • les données réellement partagées, qui exigent une synchronisation ;
  • les données propres à un appel, qui doivent rester locales ;
  • les données persistantes propres à chaque thread, qui relèvent des TSD ou du TLS.
Un programme thread-safe ne consiste pas seulement à ajouter des mutex : il faut avant tout supprimer les partages inutiles.

Who is online

Users browsing this forum: No registered users and 1 guest