IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Bien programmer en langage C

Date de publication : 28 mai 2008


IX. POSIX threads
IX-A. Systèmes multi-tâches
IX-A-1. Introduction
IX-A-2. Processus
IX-A-2-a. Exécution
IX-A-2-b. Création dynamique de processus
IX-A-3. Processus légers
IX-B. Introduction
IX-B-1. Note pour Windows.
IX-B-2. Objectif
IX-C. Hello worlds
IX-D. Données
IX-D-1. Éviter les globales 'à-la-barbare'
IX-D-2. Le cas de l'écrivain et du lecteur
IX-E. Synchronisation
IX-F. Résumé des types et des fonctions utilisés
IX-G. Ressources


IX. POSIX threads


IX-A. Systèmes multi-tâches


IX-A-1. Introduction

Force est de constater qu'un programme courant (sauf traitements lourds comme un scan d'antivirus) passe 90 à 99% de son temps à attendre des évènements extérieurs, notamment, évidemment, les entrées (stdin, lecture fichier, attentes de données reçues sur un socket...) à moins qu'on ait voulu délibérément retarder l'exécution de telle ou telle action.

Donc, un processeur est la plupart du temps en train de ne rien faire (ou alors des boucles inutiles). Dès les débuts de l'informatique, cette constatation a amené les informaticiens à réfléchir à la meilleure façon d'utiliser ce temps libre.

L'idée de répartir le temps du processeur entre plusieurs application s'est alors imposée.

Les performances des systèmes modernes reposent en grande partie sur leur possibilité de charger et exécuter plusieurs applications 'en même temps'. Cette simultanéité n'est évidemment qu'apparente, sur une machine mono-processeur. Mais elle a l'avantage d'exister et d'être efficace, et nous le constatons tous les jours sur notre PC ou notre téléphone mobile.


IX-A-2. Processus

Un processus est une application (ou un programme) en cours d'exécution. Un programme, est avant tout un fichier stocké sur un disque quelconque contenant du code exécutable.

Pour pouvoir être exécuté, il faut réunir un certain nombre de conditions :

  • charger le programme en mémoire.
  • réserver de l'espace pour les données statiques.
  • réserver de l'espace dans la pile.
  • initialiser les pointeurs d'instructions et de pile.
  • lancer l'exécution.
A un processus est donc associé un espace mémoire composé d'un programme (code exécutable) et de données (statiques et pile).


IX-A-2-a. Exécution
Dans un système multi-tâches comme Unix ou Windows (mais pas MS-DOS), "lancer l'exécution" signifie rendre le processus "éligible", c'est à dire que dès le le processeur est libre, il pourra l'exécuter réellement (il devient alors "élu").

Dès que le programme rencontre une fonction qui le fait 'attendre' (suspension temporisée, attente d'une entrée etc.) le système (en fait, l'ordonnanceur, qui est le mécanisme de répartition du temps machine) en profite pour suspendre l'exécution du processus ("suspendu"). Le premier processus éligible passe alors en mode "élu". (il "prend la main" comme on dit vulgairement).

Il faut savoir qu'il existe certaines tâches dites 'immédiates' (interruptions) qui peuvent s'exécuter en temps réel à chaque instant. C'est notamment le cas pour les traitements de bas niveau de type timer (déterminer une échéance) ou clavier, réseau, disque etc. Les évènements détéctés sont transmis au système qui détermine alors à quel processus suspendu ils correspondent et si il s'agit bien d'un évènement de déblocage. ("en attente d'un <enter>", "en attente d'une trame réseau" etc). Si c'est le cas, le processus passe en état "éligible". La suite, on la connait.

En résumé, les états d'un processus sont :

Posix threads
Il peut y avoir des schémas plus complexes, mais le principe est là.


IX-A-2-b. Création dynamique de processus
Il est possible par logiciel de créer une 'copie' ou un 'fils' du processus courant. Cela revient à créer un nouvel espace mémoire contenant une copie des données (le code reste unique).

Les valeurs des données du processus fils sont celles de l'instant de la copie (sous unixoïde, fork()). Si le processus fils modifie ses données, les données du père sont inchangées. De même, si le père modifie ses données, les données du fils sont inchangées. Les deux espaces mémoires sont complètement étanches. au point que si il fallait échanger des données entre les deux processus, il faudrait utiliser des mécanismes assez complexes (IPC : Inter Processus Communication, SHM : SHared Memory, sockets UNIX etc).

N'étant pas spécialiste des processus, j'ai peut être commis des erreurs... Remarques bienvenues.


IX-A-3. Processus légers

Dans un processus donné, il est possible de créer un processus léger appelé aussi thread qui est un espace mémoire limité à une simple pile et un code exécutable (la fonction du thread). La mémoire statique est celle du processus courant. Si plusieurs threads sont crées, ils partagent le même espace de mémoire statique, mais ils ont chacun leur propre pile (variables locales).

Ce mécanisme est simple et permet d'écrire facilement des applications multi-tâches. Les données statiques étant partagées, leur accès est direct (mais on évite les globales, on n'est pas des barbares)

Revers de la médaille, les accès aux données étant directs, et des accès concurrents étant possibles, les données peuvent être incorrectes si des précautions ne sont pas prises. Il existe donc des mécanismes de protection qui permettent de réguler l'accès aux données (sémaphores).

La gestion des threads a été normalisée par POSIX.1. C'est l'API pthreads.


IX-B. Introduction

La norme POSIX.1 fournit un ensemble de primitives permettant de réaliser des processus légers. Contrairement aux processus habituels, la mémoire est commune. Ces processus légers sont communément appelés 'threads' ou 'tâches'. Selon l'implémentation, l'ensemble des primitives est, par exemple, regroupé dans la bibliothèque libpthread.a (gcc) qu'il faut bien sûr penser à ajouter au projet (-lpthread).


IX-B-1. Note pour Windows.

Les pthreads sont disponibles ici .


IX-B-2. Objectif

Le but de cet article est de fournir les bases de l'utilisation de la bibliothèque pthread par une approche pragmatique.


IX-C. Hello worlds

Il est simple de réaliser la création, l'exécution et la fin de 2 tâches 'concurrentes'[1]. Pour cela, on utilise le type opaque (ADT) pthread_t qui va contenir le contexte de chaque tâche, et une fonction de création de tâche pthread_create() qui va associer la tâche avec son contexte, et informer l'ordonnanceur du système de son existence. L'ordonnanceur lancera la tâche dès que possible

/* ATTENTION CODE ERRONE */

#include <stdio.h>

#include <pthread.h>

static void *task_a (void *p_data)
{
   puts ("Hello world A");

   (void) p_data;
   return NULL;
}

static void *task_b (void *p_data)
{
   puts ("Hello world B");

   (void) p_data;
   return NULL;
}

int main (void)
{
   pthread_t ta;
   pthread_t tb;

   puts ("main init");

   pthread_create (&ta, NULL, task_a, NULL);
   pthread_create (&tb, NULL, task_b, NULL);

   puts ("main end");

   return 0;
}
La sortie est, par exemple, celle-ci. Attention, l'ordre dans lequel l'exécution des taches se font n'est pas défini (normal, puisque dans l'idéal, elle sont concurrentes).

main init
main end
Hello world A
Hello world B
Il est important de bien interpréter les lignes produites. Dans cet exemple, il y a un processus (l'application) et 3 tâches :

  • main()
  • task_a()
  • task_b()
Il est clair, à la vue des lignes produites, que la tâche main() se termine avant que les autres ne soient lancées. Cela signifie que, étant donné que le contexte des tâche n'existe plus, le comportement des tâches devient indéterminé. C'est pourquoi il existe la fonction de synchronisation pthread_join() qui permet de suspendre l'exécution de la tâche courante en attendant la fin d'une tâche déterminée.

#include <stdio.h>

#include <pthread.h>

static void *task_a (void *p_data)
{
   puts ("Hello world A");

   (void) p_data;
   return NULL;
}

static void *task_b (void *p_data)
{
   puts ("Hello world B");

   (void) p_data;
   return NULL;
}

int main (void)
{
   pthread_t ta;
   pthread_t tb;

   puts ("main init");

   pthread_create (&ta, NULL, task_a, NULL);
   pthread_create (&tb, NULL, task_b, NULL);

#if 1
   pthread_join (ta, NULL);
   pthread_join (tb, NULL);
#endif

   puts ("main end");

   return 0;
}
La sortie donne maintenant ceci:

main init
Hello world A
Hello world B
main end
Dans la réalité, une tâche est généralement une boucle (infinie ou non).

#include <stdio.h>

#include <pthread.h>

static void *task_a (void *p_data)
{
   int i;

   for (i = 0; i < 5; i++)
   {
      printf ("Hello world A (%d)\n", i);
   }

   (void) p_data;
   return NULL;
}

static void *task_b (void *p_data)
{
   int i;

   for (i = 0; i < 7; i++)
   {
      printf ("Hello world B    (%d)\n", i);
   }
   (void) p_data;
   return NULL;
}

int main (void)
{
   pthread_t ta;
   pthread_t tb;

   puts ("main init");

   pthread_create (&ta, NULL, task_a, NULL);
   pthread_create (&tb, NULL, task_b, NULL);

#if 1
   pthread_join (ta, NULL);
   pthread_join (tb, NULL);
#endif

   puts ("main end");

   return 0;
}
Les informations produites sont variables. Par exemple :

main init
Hello world A (0)
Hello world B    (0)
Hello world B    (1)
Hello world A (1)
Hello world A (2)
Hello world B    (2)
Hello world B    (3)
Hello world A (3)
Hello world A (4)
Hello world B    (4)
Hello world B    (5)
Hello world B    (6)
main end
ou

main init
Hello world A (0)
Hello world B    (0)
Hello world B    (1)
Hello world A (1)
Hello world A (2)
Hello world B    (2)
Hello world B    (3)
Hello world B    (4)
Hello world A (3)
Hello world A (4)
Hello world B    (5)
Hello world B    (6)
main end
mais on constate que le mécanisme de rendez-vous fonctionne et que les deux boucles sont terminées correctement. Sans le mécanisme de synchronisation :

#if 0
   pthread_join (ta, NULL);
   pthread_join (tb, NULL);
#endif
on aurait, par exemple, obtenu ceci, ce qui n'est pas loin d'une catastrophe...

main init
main end
Hello world A (0)
Hello world A (1)
Hello world B    (0)
Hello world B    (1)
Hello world B    (2)
Hello world A (2)
Hello world A (3)
Hello world B    (3)
Hello world B    (4)
Hello world A
[1] En fait, la concurrence est une notion relative. Sur un système mono-processeur, l'exécution des tâches se fait selon des critères de temps (time slicing) ou de suspensions.


IX-D. Données

L'application étant formée d'un seul processus, les données sont communes à toutes les tâches. Cela signifie qu'une donnée est partageable directement entre les processus. Le terme 'données communes' recouvre comme il se doit :

  • les données statiques
  • les données allouées
  • les données locales

IX-D-1. Éviter les globales 'à-la-barbare'

Il est possible d'utiliser le 4ème paramètre de pthread_create() pour passer l'adresse d'un objet quelconque à une tâche (variable simple, tableau, structure) au moment de la création de la tâche. Il est bien évident que la durée de vie de cette variable doit être supérieure ou égale à la durée de vie de la tâche qui l'utilise.

#include <stdio.h>

#include <pthread.h>

static void *task_a (void *p_data)
{
   if (p_data != NULL)
   {
      /* recuperer le contexte applicatif */
      char const *s = p_data;
      int i;

      for (i = 0; i < 5; i++)
      {
         printf ("'%s' (%d)\n", s, i);
      }
   }
   return NULL;
}

static void *task_b (void *p_data)
{
   if (p_data != NULL)
   {
      /* recuperer le contexte applicatif */
      char const *s = p_data;
      int i;

      for (i = 0; i < 7; i++)
      {
         printf ("'%s'     (%d)\n", s, i);
      }
   }
   return NULL;
}

int main (void)
{
   pthread_t ta;
   pthread_t tb;

   puts ("main init");

/* La chaine etant definie const sur ma machine, je force en (char*).
 * Dans la tâche, 's' est bien de type 'char const *'
 */
   pthread_create (&ta, NULL, task_a, (char*)"Tache A");
   pthread_create (&tb, NULL, task_b, (char*)"Tache B");

   pthread_join (ta, NULL);
   pthread_join (tb, NULL);

   puts ("main end");

   return 0;
}
qui produit par exemple :

main init
'Tache A' (0)
'Tache B'     (0)
'Tache B'     (1)
'Tache A' (1)
'Tache A' (2)
'Tache A' (3)
'Tache B'     (2)
'Tache B'     (3)
'Tache B'     (4)
'Tache A' (4)
'Tache B'     (5)
'Tache B'     (6)
main end

IX-D-2. Le cas de l'écrivain et du lecteur

Soit une variable partagée qui est modifiée par une tâche, et lue par une autre. Le code 'naïf' suivant :

#include <stdio.h>

#include <pthread.h>

struct shared
{
   int data;
};

struct data
{
   /* parametres */
   int nb;
   char const *sid;

   /* contexte partage' */
   struct shared *psh;
};

static void *task_w (void *p)
{
   if (p != NULL)
   {
      /* recuperer le contexte applicatif */
      struct data *p_data = p;
      int i;

      for (i = 0; i < p_data->nb; i++)
      {
         int x = p_data->psh->data;
         x++;
         p_data->psh->data = x;

         printf ("'%s' (%d) data <- %d\n", p_data->sid, i, p_data->psh->data);
      }
   }
   return NULL;
}

static void *task_r (void *p)
{
   if (p != NULL)
   {
      /* recuperer le contexte applicatif */
      struct data *p_data = p;
      int i;

      for (i = 0; i < p_data->nb; i++)
      {
         printf ("                        "
                 "'%s' (%d) data == %d\n", p_data->sid, i, p_data->psh->data);
      }
   }
   return NULL;
}

int main (void)
{
   pthread_t ta;
   pthread_t tb;

   struct shared sh =
   {
      .data = 0,
   };

   struct data da =
   {
      .nb = 5,
      .sid = "Writer",
      .psh = &sh,
   };

   struct data db =
   {
      .nb = 7,
      .sid = "Reader",
      .psh = &sh,
   };

   puts ("main init");

   pthread_create (&ta, NULL, task_w, &da);
   pthread_create (&tb, NULL, task_r, &db);

   pthread_join (ta, NULL);
   pthread_join (tb, NULL);

   puts ("main end");

   return 0;
}
provoque des abérrations comme

main init
'Writer' (0) data <- 1
                        'Reader' (0) data == 1
                        'Reader' (1) data == 2
'Writer' (1) data <- 2
'Writer' (2) data <- 3
                        'Reader' (2) data == 2
                        'Reader' (3) data == 4
'Writer' (3) data <- 4
'Writer' (4) data <- 5
                        'Reader' (4) data == 4
                        'Reader' (5) data == 5
                        'Reader' (6) data == 5
main end
où l'on constate que les valeurs lues sont parfois fausses par rapport aux valeurs réelles.

Pour se prémunir contre ce problème, on doit évaluer quelle est la 'zone critique', et ensuite la protéger avec un 'sémaphore'. C'est tout simplement un mécanisme qui va empêcher une autre tâche d'interrompre la section critique. Dans l'environnement pthread, ce mécanisme est appelé mutex. Il met en oeuvre le type opaque pthread_mutex_t et les fonctions pthread_mutex_lock() et pthread_mutex_unlock().

Voici un exemple de protection de la section critique de l'écrivain. (empêcher le lecteur d'accéder en lecture pendant l'écriture).

#include <stdio.h>

#include <pthread.h>

struct shared
{
   int data;
   pthread_mutex_t mut;
};

struct data
{
   /* parametres */
   int nb;
   char const *sid;

   /* contexte partage' */
   struct shared *psh;
};


static void *task_w (void *p)
{
   if (p != NULL)
   {
      struct data *p_data = p;
      int i;

      for (i = 0; i < p_data->nb; i++)
      {

/* debut de la zone critique */
         pthread_mutex_lock (&p_data->psh->mut);
         {
            int x = p_data->psh->data;
            x++;
            p_data->psh->data = x;
         }
         printf ("'%s' (%d) data <- %d\n", p_data->sid, i, p_data->psh->data);
         pthread_mutex_unlock (&p_data->psh->mut);
/* fin de la zone critique */

      }
   }
   return NULL;
}

static void *task_r (void *p)
{
   if (p != NULL)
   {
      struct data *p_data = p;
      int i;

      for (i = 0; i < p_data->nb; i++)
      {
         printf ("                        "
               "'%s' (%d) data == %d\n", p_data->sid, i, p_data->psh->data);
      }
   }
   return NULL;
}

int main (void)
{
   pthread_t ta;
   pthread_t tb;

   struct shared sh =
   {
      .data = 0,
      .mut = PTHREAD_MUTEX_INITIALIZER,
   };

   struct data da =
   {
      .nb = 5,
      .sid = "Writer",
      .psh = &sh,
   };

   struct data db =
   {
      .nb = 7,
      .sid = "Reader",
      .psh = &sh,
   };

   puts ("main init");

   pthread_create (&ta, NULL, task_w, &da);
   pthread_create (&tb, NULL, task_r, &db);

   pthread_join (ta, NULL);
   pthread_join (tb, NULL);

   puts ("main end");

   return 0;
}
L'amélioration est flagrante, mais insuffisante :

main init
'Writer' (0) data <- 1
'Writer' (1) data <- 2
'Writer' (2) data <- 3
                        'Reader' (0) data == 3
                        'Reader' (1) data == 4
'Writer' (3) data <- 4
'Writer' (4) data <- 5
                        'Reader' (2) data == 4
                        'Reader' (3) data == 5
                        'Reader' (4) data == 5
                        'Reader' (5) data == 5
                        'Reader' (6) data == 5
main end
Il faut aussi protéger la lecture, c'est à dire empêcher l'écrivain d'écrire pendant la lecture :

#include <stdio.h>

#include <pthread.h>

struct shared
{
   int data;
   pthread_mutex_t mut;
};

struct data
{
   /* parametres */
   int nb;
   char const *sid;

   /* contexte partage' */
   struct shared *psh;
};

static void *task_w (void *p)
{
   if (p != NULL)
   {
      struct data *p_data = p;
      int i;

      for (i = 0; i < p_data->nb; i++)
      {

/* debut de la zone critique */
         pthread_mutex_lock (&p_data->psh->mut);
         {
            int x = p_data->psh->data;
            x++;
            p_data->psh->data = x;
         }
         printf ("'%s' (%d) data <- %d\n", p_data->sid, i, p_data->psh->data);
         pthread_mutex_unlock (&p_data->psh->mut);
/* fin de la zone critique */

      }
   }
   return NULL;
}

static void *task_r (void *p)
{
   if (p != NULL)
   {
      struct data *p_data = p;
      int i;

      for (i = 0; i < p_data->nb; i++)
      {
/* debut de la zone critique */
         pthread_mutex_lock (&p_data->psh->mut);
         printf ("                        "
               "'%s' (%d) data == %d\n", p_data->sid, i, p_data->psh->data);
         pthread_mutex_unlock (&p_data->psh->mut);
/* fin de la zone critique */
      }
   }
   return NULL;
}

int main (void)
{
   pthread_t ta;
   pthread_t tb;

   struct shared sh =
   {
      .data = 0,
      .mut = PTHREAD_MUTEX_INITIALIZER,
   };

   struct data da =
   {
      .nb = 5,
      .sid = "Writer",
      .psh = &sh,
   };

   struct data db =
   {
      .nb = 7,
      .sid = "Reader",
      .psh = &sh,
   };

   puts ("main init");

   pthread_create (&ta, NULL, task_w, &da);
   pthread_create (&tb, NULL, task_r, &db);

   pthread_join (ta, NULL);
   pthread_join (tb, NULL);

   puts ("main end");

   return 0;
}
Le comportement est maintenant conforme aux attentes :

main init
'Writer' (0) data <- 1
                        'Reader' (0) data == 1
'Writer' (1) data <- 2
                        'Reader' (1) data == 2
'Writer' (2) data <- 3
                        'Reader' (2) data == 3
'Writer' (3) data <- 4
                        'Reader' (3) data == 4
'Writer' (4) data <- 5
                        'Reader' (4) data == 5
                        'Reader' (5) data == 5
                        'Reader' (6) data == 5
main end

IX-E. Synchronisation

Nous avons pu constater qu'en environnement multi-tâches, l'ordre d'exécution des instructions de 2 tâches différentes était indéterminé. C'est pourquoi il existe un mécanisme permettant la synchronisation. Par exemple, bloquer la tâche courante en attente d'un évènement ou la débloquer dès le déclenchement de cet évènement.

Dans l'environnement pthread, on parle de 'condition'. L'objet est pthread_cond_t, les fonctions sont pthread_cond_wait() et pthread_cond_signal().

Dans l'exemple initial, les deux tâches s'exécutaient de façon désordonnée. Voici un mécanisme de synchronisation de la tâche B par la tâche A. Il a fallu ajouter une suspension dans la tâche A afin de laisser du temps à B de s'exécuter.

#include <stdio.h>

#include <pthread.h>

#ifdef WIN32
/* MS-Windows */
#include <windows.h>
#define msleep(ms) Sleep(ms)
#else
/* unixoides */
#include <unistd.h>
#define msleep(ms) usleep((ms * 1000))
#endif

struct shared
{
   pthread_mutex_t mut;
   pthread_cond_t synchro;
};

struct data
{
   /* parametres */
   int nb;
   char const *sid;

   /* contexte partage' */
   struct shared *psh;
};

static void *task_a (void *p)
{
   if (p != NULL)
   {
      struct data *p_data = p;
      struct shared *psh = p_data->psh;
      int i;

      for (i = 0; i < p_data->nb; i++)
      {
         pthread_mutex_lock (&psh->mut);
         printf ("'%s' (%d)\n", p_data->sid, i);
         pthread_cond_signal (&psh->synchro);
         pthread_mutex_unlock (&psh->mut);
         msleep (1000);
      }
   }
   return NULL;
}

static void *task_b (void *p)
{
   if (p != NULL)
   {
      struct data *p_data = p;
      struct shared *psh = p_data->psh;
      int i;

      for (i = 0; i < p_data->nb; i++)
      {
         pthread_mutex_lock (&psh->mut);
         pthread_cond_wait (&psh->synchro, &psh->mut);
         printf ("                        "
                 "'%s' (%d)\n", p_data->sid, i);
         pthread_mutex_unlock (&psh->mut);
      }
   }
   return NULL;
}

int main (void)
{
   pthread_t ta;
   pthread_t tb;

   struct shared sh =
   {
      .mut = PTHREAD_MUTEX_INITIALIZER,
      .synchro = PTHREAD_COND_INITIALIZER,
   };

   struct data da =
   {
      .nb = 5,
      .sid = "task A",
      .psh = &sh,
   };

   struct data db =
   {
      .nb = 4,
      .sid = "Task B",
      .psh = &sh,
   };

   puts ("main init");

   pthread_create (&ta, NULL, task_a, &da);
   pthread_create (&tb, NULL, task_b, &db);

   pthread_join (ta, NULL);
   pthread_join (tb, NULL);

   puts ("main end");

   return 0;
}

IX-F. Résumé des types et des fonctions utilisés

Types et fonctions Description
pthread_t Contexte de tâche
pthread_create() Création d'une tâche
pthread_join() Attente de fin de tâche
pthread_mutex_t Sémaphore d'exclusion mutuelle
pthread_mutex_lock() Début de zone critique
pthread_mutex_unlock() Fin de zone critique
pthread_cond_t Condition
pthread_cond_wait() Mise en attente
pthread_cond_signal() Déclenchement
info Les essais ont été faits sous Windows 98 avec Dev-C++ et sa bibliothèque pthread. L'ordonnanceur de Windows étant préemptif, les aberrations de fonctionnement sont claires et évidentes. Les essais réalisés sous Linux embarqué (Kernel 2.4 pour Power PC) sont moins parlants, car sur ma configuration de test, l'ordonnanceur n'est pas préemptif. Il semble aussi que, sous Windows XP, l'ordonnanceur ne soit pas préemptif (time slicing), mais coopératif... A moins que j'interprète mal le fonctionnement... Remarques bienvenues.
Les remarques reçues font état des faits suivants :

  • L'ordonnanceur de GNU/Linux 2.4 est soit coopératif, soit préemptif selon une option de compilation du noyau.
  • L'ordonnanceur de GNU/Linux 2.6 est préemptif.
  • L'ordonnanceur de MS/Windows 98 est coopératif (blocage possible par une 'boucle blanche')..
  • L'ordonnanceur de MS/Windows NT/XP est préemptif (évènements + time slicing).

IX-G. Ressources

Pour approfondir, je recommande cet excellent site en anglais, bien sûr.

 

Valid XHTML 1.1!Valid CSS!

Copyright © 2008 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.