TP2 - Part 2 : La norme POSIX

La bibliothèque pthread correspond à une implémentation de la norme POSIX1003.1c. Elle fournit des primitives pour créer et gérer des threads / processus légers. Dans le domaine des threads du monde unixien, cette norme s’est imposée. Une documentation plus complète de la librairie pthreads est disponible dans le man (man libpthread , man pthread_xxx et/ou man pthreads).

Voici un premier programme utilisant des fonctions POSIX (code téléchargeable ici):

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NB_THREADS	3

void *travailUtile(void *null)
{
    int i;
    double resultat=0.0;
    for (i=0; i<1000000; i++)
    {
        resultat = resultat + (double)random();
    }
    printf("resultat = %e\n",resultat);
    pthread_exit((void *) 0);
}

int main (int argc, char *argv[])
{
    pthread_t thread[NB_THREADS];
    pthread_attr_t attr;
    int rc, t;
    void *status;

    /* Initialisation et activation d’attributs */
    pthread_attr_init(&attr); //valeur par défaut
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); //attente du thread possible

    for(t=0; t<NB_THREADS; t++)
    {
        printf("Creation du thread %d\n", t);
        rc = pthread_create(&thread[t], &attr, travailUtile, NULL); 
        if (rc)
        {
            printf("ERROR; le code de retour de pthread_create() est %d\n", rc);
            exit(-1);
        }
    }

    /* liberation des attributs et attente de la terminaison des threads */
    pthread_attr_destroy(&attr);
    for(t=0; t<NB_THREADS; t++)
    {
        rc = pthread_join(thread[t], &status);
        if (rc)
        {
            printf("ERROR; le code de retour du pthread_join() est %d\n", rc);
            exit(-1);
    }
    printf("le join a fini avec le thread %d et a donne le status= %ld\n",t, (long)status);
    }

    pthread_exit(NULL);
}
Ce programme crée 3 threads qui font des choses très utiles et attend leur terminaison.
  1. Testez ce programme (la compilation de programme utilisant des threads POSIX se fait avec l’option -lpthread)
  2. Modifiez ce programme pour afficher la valeur par défaut de la taille de la pile (stack) des threads. Augmentez-la ensuite d’un méga octet pour les threads crées (avant leur création). Vous pouvez utiliser les deux functions ci-dessous :
  3. Modifiez le programme pour passer en argument à chaque thread la valeur qu’il doit additionner à la variable resultat, cette valeur doit être différente pour chaque thread
  1. Créez un programme qui utilise 4 threads pour le calcul d’un produit scalaire (2 vecteurs de 400 éléments chacun par exemple). Chaque thread fera un quart du produit qu’il stockera dans une variable temporaire. La modification du résultat global du produit scalaire par chaque thread sera effectuée dans une section critique protégée par un mutex (sémaphore binaire). Ce résultat sera affiché à la fin de l’exécution des 4 threads. Voici le squelette de votre programme:


    
        #include <pthread.h>
        #include <stdio.h>
        #include <stdlib.h>
        #define VECTOR_LENGTH 400
        #define NB_THREADS	4
    
        int vec1[VECTOR_LENGTH]; //Vector 1
        int vec2[VECTOR_LENGTH]; //Vector 2
        int res;
    
        void *computeDotProduct(void * n)
        {
            //Compute the dot product of 100 elements of vec1 and vec2
            //Update the variable res --> Need of synchronisation here
        }
        
        int main (int argc, char *argv[])
        {
            //Initialise vec1 and vec2
            for(t=0;t<VECTOR_LENGTH;t++){
                vec1[i] = ...
                vec2[i] = ...
            }        
    
            //Create 4 threads to compute the result
    
            //Remove threads
        }
    
  2. Nous souhaitons à présent faire afficher le résultat par un thread différent. Pour cela chaque thread incrémente un compteur. Le dernier thread (le nombre total est connu) relâche une variable conditionnelle (Voir Cours 2 : Communication et synchronisation interprocessus) à qui permet au thread d’affichage de faire son boulot.


  3. Nous voulons à présent utiliser une mémoire partagée pour stocker les 2 vecteurs, les résultats intermédiaires, le résultat final ainsi que le compteur. Pour faciliter la tâche, nous créerons une structure qui contient l’ensemble de ces éléments. La compilation doit s’effectuer avec l’option –lrt (inclusion de la librairie temps réel). Une mémoire partagée :

Refaire l’exercice précédent de la section « Besoin de synchronisation » en utilisant les files de messages.

Les E/S asynchrones présentent l’avantage de pouvoir initier une requête d’E/S (par exemple une lecture d’un fichier sur disque) et de faire autre chose en attendant la complétion de cette dernière.

La structure de base qu’utilisent les API aio (asynchronous Input/Output) est la structure aiocb. Elle caractérise l’entrée /sortie à effectuer et contient un certain nombre de champs dont (man aio):

Voici un exemple montrant une utilisation basique d’une E/S asynchrone :


    #include <aio.h>
    #include <stdio.h>

    int main(int argc, char * argv[])
    {
        if(argc != 2)
        {
            printf("Usage: %s {filename}\n", argv[0]);
            return -1;
        }
        struct aiocb cb; // bloc de contrôle de l’E/S asynchrone
        struct aiocb * cbs[1];
        //Ouverture du fichier (spécifié en argument) sur lequel l’E/S va être effectuée 
        FILE * file = fopen(argv[1], "r+");
        
        //definition du bloc de contrôle de l’entrée/sortie
        cb.aio_buf = malloc(11);
        cb.aio_fildes = fileno(file); //récupérer le descripteur d’un fichier à partir de  son nom
        cb.aio_nbytes = 10;
        cb.aio_offset = 0;

        //lancer la lecture
        aio_read(&cb);

        //Suspension du processus dans l’attente de la terminaison de la lecture
        cbs[0] = &cb;
        aio_suspend(cbs, 1, NULL);

        printf("operation AIO a retourne %d\n", aio_return(&cb));
        
        return 0;
    }
    

Il est évident que ce programme ne fait rien d’autre qu’implémenter une lecture synchrone avec les API des E/S asynchrones. L’idéal serait de lancer la lecture et de continuer à faire autre chose et être notifié de la fin de l’opération d’E/S par l’OS.

Pour qu’un signal soit envoyé au processus effectuant une E/S asynchrone lorsque celle-ci est terminée, on utilise le champ aio_sigevent de la structure aiocb, cette structure définie la notification de la complétion de l’E/S demandée. Elle est constituée des champs suivants :

 
    struct aiocb my_aiocb

    /* AIO Signal configuration */
    my_aiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    my_aiocb.aio_sigevent.sigev_signo = SIGIO;
    my_aiocb.aio_sigevent.sigev_value.sival_int = 0;         

Pour utiliser ce type de mécanisme, il faut définir dans la structure de contrôle de l’E/S le signal (entre SIGRTMIN et SIGRTMAX) qui va nous notifier la complétion de cette dernière et définir une fonction à exécuter à la réception de ce signal avec la primitive sigaction(). Dans sigev_value, on peut mettre une information à envoyer avec le signal pour, par exemple, différencier les E/S dont on reçoit le signal de complétion.

 
    /* Set up the signal handler */  
    struct sigaction sig_act;
    sigemptyset(&sig_act.sa_mask);
    sig_act.sa_flags = SA_RESTART|SA_SIGINFO;
    /* Function to call */
    sig_act.sa_sigaction = aio_completion_handler;

    /* Link the AIO request with the Signal Handler, sigev_signo is the chosen sigev_signo*/
    sigaction(SIGIO, &sig_act, NULL);

Faites un programme (mono thread) qui lance 2 lectures et une écriture sur des fichiers (différents ou pas) et qui affiche ce qui a été lu pour les lectures et un acquittement pour l’écriture.

Reprenez l’exercice du produit scalaire, mais le scénario sera, cette fois-ci, plus réaliste. En effet, on suppose que les vecteurs de données se trouvent dans des fichiers différents.

  1. Créez 2 fichiers où chacun contient l’un les vecteurs dont on calcule le produit scalaire.
  2. Créez un programme qui envoie 4 requêtes d’E/S asynchrones pour lire les 4 moitiés (ou quarts) de vecteur à donner pour traitement à 2 threads que l’on créera dès réception des données. Un dernier thread affichera le résultat. Choisissez le moyen de communication qui vous convienne pour l’envoi des données aux threads.