Ressources informatiques

Ressources informatiques

Ressources informatiques

Utiliser Thread et MUTEX (Librairie (C11))

Documentation : en.cppreference.com > Concurrency support library

Lancement et terminaison Variable partagée et MUTEX Cas particulier des types de bases > atomic

Lancement et terminaison

Syntaxe des fonctions utilisées

thrd_create
#include <threads.h>
int thrd_create( thrd_t *thr, thrd_start_t func, void *arg );

Démarre un nouveau thread dans le processus appelant.

thrd_join

Attend que le thread identifié par le paramètre thr se termine.

#include <threads.h>
int thrd_join( thrd_t thr, int *res );

Exemple : thread1.c

/* 
* Description : Programme qui lance un thread. Ce thread affiche un message qui lui a été passé 
*               en paramètre à son lancement.
* Compilation : gcc -o thread1 -std=c11 -pthread  thread1.c
*/
#include <stdio.h>      // pour perror(), printf()
#include <threads.h>    // pour les thread

static int threadAfficher(void * argument);

int main() {
   thrd_t  idThread; // Identifiant du thread

   char *message = (char *)"bonjour";
   int retourThread;

   // Démarrer un thread
   if (thrd_create (&idThread, &threadAfficher, (void *)message) != thrd_success)
   {perror("thrd_create() :"); return -1;}
   printf("Thread créé.\n");

   // Attendre la fin du thread
   if (thrd_join(idThread, &retourThread) != thrd_success)
   {perror("thrd_join() :"); return -1;}

   printf("Thread terminé avec le code %d\n", retourThread);

   return 0;
}

static int threadAfficher(void * argument) {
   // Afficher le message 
   const char * message = (const char *)argument;
   printf("Thread : %s\n", message);

   return thrd_success;
}
Résultats d'exécutions
doe@serveur:~$ ./thread1
Thread créé.
Thread : bonjour
Thread terminé avec le code 0

J'ajoute une pause de 10 s avant l'intruction return thrd_success; dans le thread. Cela me permet d'exécuter le programme thread1 en arrière plan à l'aide du & et de saisir la commande ps -Lf afin de visualiser les threads en cours d'exécution. Le thread père et son identifiant sont surlignés en bleu, le thread fils et son identifiant sont surlignés en vert. Le thread père correspond à la fonction main(). Les deux threads ont le même pid car ils sont encapsulés dans le même processus.

doe@serveur:~$ ./thread1 & ps -LF
[1] 1039
Thread créé.
Thread : bonjour
UID          PID    PPID     LWP  C NLWP    SZ   RSS PSR STIME TTY          TIME CMD
doe          970     969     970  0    1  2061  4988   0 16:18 pts/0    00:00:00 -bash
doe         1039     970    1039  0    2  2678   536   1 16:24 pts/0    00:00:00 ./thread1
doe         1039     970    1041  0    2  2678   536   1 16:24 pts/0    00:00:00 ./thread1
doe         1040     970    1040  0    1  2463  3448   0 16:24 pts/0    00:00:00 ps -LF

Variable partagée et MUTEX

Caractéristiques d'un thread

Deux threads (activités) d'un même processus possède une zone de données communes (ZDC) : cela signifie qu'ils pourront partager des variables.
Cependant il NE DOIVENT PAS y accèder simultanément car cela risque de produire un résultat incohérent.
Il est donc nécessaire de rendre l'accès à la variable exclusif (c'est à dire que si un thread accède à une variable pour accéder à sa valeur ou la modifier, l'autre thread ne pourra y accéder). Cela est réalisé à l'aide d'un sémaphore d'exclusion mutuelle appelé MUTEX.
Il est important que le temps d'accès exclusif à la variable soit le plus court possible afin de ne pas nuire à la fluidité du programme.

Syntaxe des fonctions utilisées

#include <threads.h>
int mtx_init( mtx_t * mutex, int type ); // initalise le sémaphore
int mtx_lock( mtx_t * mutex )            // verrouille le sémaphore
int mtx_unlock( mtx_t * mutex );         // déverrouille le sémaphore

Exemple

/* 
* description : Programme qui lance deux thread : 
* le premier incrémente une variable partagée toutes les secondes pendant dix secondes ; 
* Le second affiche le contenu de la variable partagée toutes les secondes pendant dix secondes. 
* compilation : gcc -o thread2 -std=c11 -pthread  thread2.c
*/
#include <stdio.h>   // pour perror(), printf()
#include <threads.h> // pour les thread
#include <unistd.h>  // pour sleep()

static int threadAfficher(void *argument);
static int threadModifier(void *argument);

int x = 0; // Variable partagee
mtx_t mutex;

int main()
{
    thrd_t idThreadAfficher; // Identifiant du thread afficher
    thrd_t idThreadModifier; // Identifiant du thread modifier

    char *message1 = (char *)"Thread afficher";
    char *message2 = (char *)"Thread modifier";

    int retourThread;

    // Initialiser le mutex
    mtx_init(&mutex, mtx_plain);

    // Créer le premier thread
    if (thrd_create(&idThreadModifier, &threadModifier, (void *)message2) != thrd_success)
    {
        perror("thrd_create() :");
        return -1;
    }
    printf("Thread modifier créé.\n");

    // Créer le second thread
    if (thrd_create(&idThreadAfficher, &threadAfficher, (void *)message1) != thrd_success)
    {
        perror("thrd_create() :");
        return -1;
    }
    printf("Thread afficher créé.\n");

    // Attendre la fin du second thread
    if (thrd_join(idThreadAfficher, &retourThread) != thrd_success)
    {
        perror("pthread_join() afficher :");
        return -1;
    }
    printf("Thread afficher terminé avec le code %d\n", retourThread);

    // Attendre la fin du thread
    if (thrd_join(idThreadModifier, &retourThread) != thrd_success)
    {
        perror("thrd_join() :");
        return -1;
    }

    printf("Thread modifier terminé avec le code %d\n", retourThread);

    return 0;
}

static int threadModifier(void *argument)
{
    // Afficher le message
    const char *message = (const char *)argument;
    printf("Thread : %s démarré\n", message);

    for (int i = 0; i < 10; i++)
    {
        // Vérouiller le MUTEX
        mtx_lock(&mutex);

        // Incrémenter la variable partagée
        ++x;

        // Déverrouiller le MUTEX
        mtx_unlock(&mutex);

        sleep(1);
    }

    return thrd_success;
}

static int threadAfficher(void *argument)
{
    const char *message = (const char *)argument;
    int xLoc; // variable locale

    printf("Thread : %s démarré\n", message);

    // Toutes les secondes pendant 10 secondes
    for (int i = 0; i <10; i++)
    {
        // Verrouiller le MUTEX
        mtx_lock(&mutex);

        // Copier la variable partagée dans une variable locale
        xLoc = x;

        // Déverrouiller le MUTEX
        mtx_unlock(&mutex);

        // Afficher la variable locale
        printf("%s : x vaut %d\n", message, xLoc);
        fflush(stdout);

        sleep(1);
    }
    return thrd_success;
}

L'affichage de la valeur de la variable partagée est effectuée après copie dans une variable locale afin de minimiser la durée pendant laquelle le MUTEX est vérouillé.

Résultat d'exécution
doe@serveur:~$ ./thread2
Thread modifier créé.
Thread afficher créé.
Thread : Thread afficher démarré
Thread afficher : x vaut 0
Thread : Thread modifier démarré
Thread afficher : x vaut 2
Thread afficher : x vaut 2
Thread afficher : x vaut 3
Thread afficher : x vaut 5
Thread afficher : x vaut 5
Thread afficher : x vaut 6
Thread afficher : x vaut 7
Thread afficher : x vaut 9
Thread afficher : x vaut 9
Thread afficher terminé avec le code 0
Thread modifier terminé avec le code 0

Variable partagée et type atomic

Les variables partagées correspondant à des types de bases peuvent être déclarée comme atomic ce qui permet de garantir l'unicité de la modification de la variable si et seulement si elles effectuent uniquement des opérations atomiques

/*
 * description : Programme qui lance deux thread :
 * qui incrémentent différentes variables.
 * compilation : gcc -o thread3 -std=c11 -pthread  thread3.c
 */
#include <stdio.h>   // pour perror(), printf()
#include <threads.h> // pour les thread
#include <unistd.h>  // pour sleep()
#include <stdatomic.h>

static int threadModifier(void *argument);

int x = 0;   // Variable partagee
int x2 = 0;   // Variable partagee
atomic_int xAtomic= 0;  // Variable partagee atomic
atomic_int xAtomic2= 0; // Variable partagee atomic
int xNonProtege = 0; // Ne pas faire ca : pour les tests
mtx_t mutex; // sémaphore pour la variable x
mtx_t mutex2; // sémaphore pour la variable x2

int main()
{
    thrd_t idThreadModifier1; // Identifiant du thread modifier
    thrd_t idThreadModifier2; // Identifiant du thread modifier

    char *message1 = (char *)"Thread modifier 1";
    char *message2 = (char *)"Thread modifier 2";

    int retourThread;
    // Initialiser le mutex
    mtx_init(&mutex, mtx_plain);

    // Créer les thread
    if (thrd_create(&idThreadModifier1, &threadModifier, (void *)message1) != thrd_success)
    {
        perror("thrd_create() :");
        return -1;
    }
    printf("Thread modifier 1 créé.\n");

    if (thrd_create(&idThreadModifier2, &threadModifier, (void *)message2) != thrd_success)
    {
        perror("thrd_create() :");
        return -1;
    }
    printf("Thread modifier 2 créé.\n");

    // Attendre la fin du thread
    if (thrd_join(idThreadModifier2, &retourThread) != thrd_success)
    {
        perror("thrd_join() :");
        return -1;
    }

    printf("Thread modifier 2 terminé avec le code %d\n", retourThread);

    // Attendre la fin du thread
    if (thrd_join(idThreadModifier1, &retourThread) != thrd_success)
    {
        perror("thrd_join() :");
        return -1;
    }

    printf("Thread modifier 1 terminé avec le code %d\n", retourThread);
   
    printf("\n---------------------------------------\n");
    printf("Variable partagée protégée x : \t\t\t\t%d > 2000000 attendu\n", x);
    printf("Variable partagée non protégée xNonProtege : \t\t%d > 2000000 attendu\n", xNonProtege);
    printf("Variable atomic opéraion atomique xAtomic : \t\t%d > 2000000 attendu\n", xAtomic);
    printf("---------------------------------------\n");
    printf("Variable partagée protégée x2 : \t\t\t%d > 4000000 attendu\n", x2);
    printf("Variable atomic opéraion non atomique xAtomic2 : \t%d > 4000000 attendu\n", xAtomic2);

    return 0;
}

static int threadModifier(void *argument)
{
    // Afficher le message
    const char *message = (const char *)argument;
    printf("Thread : %s démarré\n", message);

    for (int i = 0; i < 1000000; i++)
    {
		mtx_lock(&mutex);
        ++x;             // variable protégée
        mtx_unlock(&mutex);
        ++xNonProtege;  // variable non protégée : Ne pas faire ça
        ++xAtomic;      // variable atomic effectuant une opération atomique -> OK

        mtx_lock(&mutex2);
        x2 = x2 + 2;          // variable protégée
        mtx_unlock(&mutex2);
        xAtomic2 = xAtomic2 +2; // variable atomic effectuant une opération non atomique -> NOK : Ne pas faire ça
    }

    return thrd_success;
}
doe@serveur:~$ ./thread3
Thread modifier 1 créé.
Thread modifier 2 créé.
Thread : Thread modifier 2 démarré
Thread : Thread modifier 1 démarré
Thread modifier 2 terminé avec le code 0
Thread modifier 1 terminé avec le code 0

---------------------------------------
Variable partagée protégée x :                          2000000 > 2000000 attendu
Variable partagée non protégée xNonProtege :            1987723 > 2000000 attendu
Variable atomic opéraion atomique xAtomic :             2000000 > 2000000 attendu
---------------------------------------
Variable partagée protégée x2 :                         4000000 > 4000000 attendu
Variable atomic opéraion non atomique xAtomic2 :        3898044 > 4000000 attendu