Blog ENI : Toute la veille numérique !
💥 Un livre PAPIER acheté
= La version EN LIGNE offerte pendant 1 an !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici

Exécution parallèle

Introduction

Le sujet de l’exécution de tâches en parallèle nécessite que nous définissions quelques notions au préalable. Lorsqu’une application que vous avez programmée effectue son travail de façon séquentielle, vous n’avez pas à vous préoccuper des difficultés inhérentes à la programmation parallèle. Ce sont ces difficultés que nous allons définir, lorsque vous souhaitez que votre application soit capable d’effectuer deux tâches en même temps.

Pour rendre parallèle l’exécution de deux tâches, vous pouvez vous référer à la première recette pour créer un nouveau processus et à la recette "Créer un nouveau thread" pour lancer un thread. Dans ces deux cas, le système d’exploitation crée un nouveau fil d’exécution. Mais ces deux façons de procéder diffèrent parce que la création d’un nouveau processus consiste à en dupliquer un. Le fils résultant de l’opération est un processus à part entière, bien distinct de son père. Au contraire, avec les threads, tous ceux appartenant au même processus ont accès au même environnement (variables globales, descripteurs de fichiers ouverts...). Le choix entre ces deux solutions...

Créer un nouveau processus

Problème

Vous souhaitez que votre programme effectue plusieurs tâches en parallèle en créant des processus dédiés.

Solution

Utilisez fork() pour dupliquer le processus et faites exécuter la tâche dédiée au processus fils.

Discussion

L’appel système fork() duplique le processus en cours d’exécution et les deux processus résultant de cet appel sont le processus père ayant appelé fork() qui connaît l’identifiant de processus (PID) de son fils par la valeur retour de fork(), et le processus fils pour qui fork() a renvoyé la valeur nulle. Après l’appel à fork(), le processus fils dispose des mêmes variables que son père, et leur contenu est le même. Mais les processus étant distincts, un changement dans l’un des processus n’est pas répercuté dans l’autre processus.

Le code générique pour dupliquer un processus est le suivant :


int pid; 
switch ((pid = fork())) 
  { 
  case -1: 
    fprintf (stderr, "fork() a échoué\n"); 
    exit (EXIT_FAILURE); 
  case 0: 
    fonction_executee_par_le_fils (arguments); 
    break; 
  default: 
    fonction_executee_par_le_pere (arguments); 
  } 
 
fonctions_executees_par_le_pere_et_le_fils ();
 

Le processus fils et le processus...

Éviter les processus zombies

Problème

Votre programme génère des processus zombies et vous souhaitez vous en débarrasser proprement.

Solution

Lors de la réception d’un signal SIGCHLD, utilisez une fonction de la famille wait() pour prendre connaissance de la fin d’exécution d’un processus fils devenu zombie.

Discussion

Lorsqu’un processus s’arrête, le système ne le fait pas disparaître mais lui donne un état, dit zombie. Il ne tourne plus, mais reste dans la table des processus. Pour le faire disparaître complètement, il faut lire son code retour avec wait(), waitpid(), wait3() ou wait4(). L’obtention de ce code retour est discutée dans la recette du chapitre suivant : "Récupérer le code de retour d’un programme qui s’est terminé".

Lorsqu’un processus s’arrête, le système envoie de plus un signal SIGCHLD au processus père. Il est possible de se servir de cela pour exécuter la balayette à zombies. Ce terme désigne le code suivant, à savoir une boucle qui appelle waitpid() tant qu’il existe des processus fils devenus zombies et les fait ainsi disparaître de la table des processus.


while (0 < waitpid (-1, NULL, WNOHANG));
 

La mise en place d’un gestionnaire de signaux est abordée dans la première recette du...

Créer un nouveau thread

Problème

Vous souhaitez que votre programme effectue plusieurs tâches en parallèle en créant des threads dédiés.

Solution

Exécutez la fonction à paralléliser à l’aide de pthread_create()ou, avec un compilateur compatible C11, de thrd_create().

Discussion

La fonction pthread_create() prend quatre arguments. Le premier est un pointeur sur un entier de type pthread_t. Cet entier contiendra l’identifiant du thread ainsi créé. Le deuxième argument permet de spécifier des options au thread. Le troisième et le quatrième sont relatifs à la fonction à exécuter dans le thread. Le troisième est un pointeur sur cette fonction, dont voici le prototype :


void *fonction (void *arg);
 

Le quatrième argument, quant à lui, est un pointeur qui sera fourni tel quel à la fonction. L’exemple suivant effectue un long calcul (une simple addition) dans un thread pendant que le programme principal effectue son travail en testant de temps en temps si le thread a fini son long calcul.


#include <stdio.h> 
#include <stdlib.h> 
 
#include <pthread.h> 
 
typedef struct 
{ 
  int a; 
  int b; 
  int result; 
  char done; 
} addition_t; 
 
void * 
addition (void *arg) 
{ 
  addition_t *ad = arg; 
  ad->result = ad->a + ad->b; 
  ad->done = 1; 
  pthread_exit (NULL); 
} 
 
int 
main () 
{ 
  int i; 
  addition_t table[10]; 
  pthread_t pid[10]; 
  int threads_remaining; 
 
/* Initialisation des 10 structures. */ 
  for (i = 0; i < 10; i++) 
    { 
      table[i].a = i; 
      table[i].b = 10 - i; 
      table[i].done = 0; 
    } 
 
/* Lancement des 10 threads avec les structures en argument. */ 
  for (i = 0; i < 10; i++) 
    { 
      if (0 != pthread_create (&(pid[i]), NULL, addition, &(table[i]))) 
        { 
          fprintf (stderr, 
            "Problème avec pthread_create() (i=%d)\n", i); 
          exit (EXIT_FAILURE); 
        } 
    } 
 
/* Attente des résultats et affichage de ceux-ci. */ ...

Limiter l’accès à une section critique

Problème

Plusieurs programmes ou threads ont accès à la même ressource et vous souhaitez que chacun attende son tour.

Solution

Utilisez un jeton, un sémaphore ou un mutex.

Discussion

Le sujet de l’exclusion mutuelle est vaste. Nous nous limiterons à deux cas. Le premier est celui où un fil d’exécution (thread ou processus) accède à une donnée en écriture et l’autre en lecture seule. Le second est celui où plusieurs fils d’exécution tenteront d’accéder à une ressource avec possibilité de modification de la ressource.

Le premier cas est relativement simple. Il suffit de disposer d’un jeton, qu’un seul fil d’exécution peut modifier. Lorsqu’il modifie ce jeton, c’est pour indiquer à un autre fil d’exécution qu’il a fini avec la modification des ressources, et que l’autre fil d’exécution et celui-là seul, peut modifier la ressource. Une implémentation simple consiste à disposer d’un entier, accessible à tous les fils d’exécution en lecture, et que seul celui dont l’identifiant est contenu dans l’entier peut accéder en écriture. Lorsque ce dernier a fini de travailler avec la ressource, il place l’identifiant d’un autre fil d’exécution dans cet entier. C’est la méthode du jeton qui a été choisie dans l’exemple au début de la recette précédente. Sous le champ done de la structure addition_t que nous pensons être un drapeau se cache un identifiant de fil d’exécution. Le thread principal dispose de l’identifiant 1 et le thread d’addition 0 (la valeur 2 que prend ensuite le caractère est un recyclage de variable). La ressource partagée consiste en le contenu de la structure addition_t. La méthode du jeton pose un problème dans certains cas. En effet, si plus aucun fil d’exécution ne possède le jeton, l’application est alors bloquée car plus personne n’aura la possibilité de changer le jeton. De plus, chaque fil d’exécution doit implémenter un gestionnaire de jetons dès qu’il y a plus de deux fils...

Communiquer entre deux processus distincts

Problème

Vous souhaitez partager des données entre deux processus.

Solution

Choisissez parmi l’une de ces solutions :

  • Les sockets locales (sockets UNIX) ;

  • Un tube nommé (fichier FIFO) ;

  • La mémoire partagée ;

  • Un fichier.

Discussion

Avant de discuter ces quatre solutions, signalons qu’il existe d’autres méthodes, en particulier celles utilisant le réseau. Pour celles-ci, orientez-vous vers la lecture des recettes du chapitre "Réseau". Elles ne sont pas à négliger. Peu de différences existent d’ailleurs entre l’utilisation de sockets locales et l’utilisation de sockets TCP/IP.

Les sockets locales et les tubes nommés sont très proches en termes d’utilisation, alors que leur concept est différent. Dans les deux cas, nous créons un fichier spécial, l’un de type socket et l’autre de type tube nommé. Par ailleurs un programme peut écrire dedans et un autre y lire. Ce qui les distingue est leur initialisation respective, qui montre la différence de concept. En effet, le programme qui crée la socket est un serveur, et le programme qui s’y connecte, un client. Pour un tube nommé, un programme le crée, puis tout programme qui ouvre ce tube peut y lire et y écrire, sans notion de se connecter à un autre programme.

Les deux fonctions ci-dessous permettent de créer une socket locale et de s’y connecter. Ce sont...