diff --git a/content/progsys/5_les-processus_legers/files/presentation.pdf b/content/progsys/5_les-processus_legers/files/presentation.pdf new file mode 100644 index 0000000..b3b8aad Binary files /dev/null and b/content/progsys/5_les-processus_legers/files/presentation.pdf differ diff --git a/content/progsys/5_les-processus_legers/index.md b/content/progsys/5_les-processus_legers/index.md new file mode 100644 index 0000000..1ffd885 --- /dev/null +++ b/content/progsys/5_les-processus_legers/index.md @@ -0,0 +1,531 @@ +--- +title: "Les processus légers" +categories: ["C", "programmation", "threads"] +date: 2018-10-02 +--- + +Les processus légers son similaires aux processus dans la mesure ou il permettent +l'exécution d'actions concurrentes (réception de données et mise à jour de +l'interface graphique par exemple). Ils permettent aussi une véritable exécution +en parallèle sur une architecture multi-cœurs / multi-processeurs. + +Contrairement aux processus, les threads partagent tous un même espace mémoire +(mais avec une pile d'exécution différente) + +Il comportent plusieurs avantages : + + - le partage d'information est plus simple avec les processus légers (pas de + mise en place d'*IPC*) + - ils sont... légers, de l'ordre de facteur 10 pour leurs création par le + noyau + - le partage d'informationa entre processus légers est de facto plus simple + puisqu'ils partagent le même espace mémoire + +Ils partagent : + + - comme on le disait, leur espace mémoire + - le *PID* et *PPID* (id de processus et id du processus parent) + - la table des fichiers ouverts + - le gestionnaire de signaux + +Ils ne partagent pas : + + - le *thread ID*, id de processus léger + - leurs piles d'exécution ; pas de partage de variables locales lors d'appels + de fonctiions + - `errno` car cela présenterai un risque de "collision" entre threads + - la mémoire locale du processus appelée *Thread Local Storage* + +## L'API Thread POSIX (Pthreads) + +Issue de POSIX .1c et intégrée dans la norme SUSv3, cette API est devenue +standards sous les système de type UNIX. Elle couvre les types de données, la +gestion des processus légers (creation, annulation, attente, etc.), les verrous, +les exclusions mutuelles, les variables conditionnelles etc. + +Voir [Pthread][w_pthreads] sur Wikipedia, ou la [version anglaise][w_pthreads_e] +un peu plus complète. + +[w_pthreads]:https://fr.wikipedia.org/wiki/Threads_POSIX +[w_pthreads_e]:https://en.wikipedia.org/wiki/POSIX_Threads + +### convention de l'API + +Habituellement, un processus renvoie 0 ou un entier positif en cas de succès. En +cas d'échec il renvoie -1 et `errno` est positionné. Dans le cadre de Pthreads, +0 est renvoyé en cas de succès et seulement `errno` est positionné en cas +d'échec. + +## Compiler un programme C avec des threads + +Pour compiler un programme C faisant appel au threads, il faut rajouter +`-ptread` à gcc + +```shell +$ gcc -Wall -pthread mon_prog.c -o monprog.bin +``` + +## Création de processus légers + +```C +#include + +int pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*start_routine) (*void), void *arg); +``` + + - **start_routine** pointe vers la fonction appelée lors de la création du + processus léger. + - **arg** sera passé en argument de cette fonction. + - **thread** sera rempli avec l'ID du thread. + - **attr** permet de définir des attributs pour le processus léger. + +Par convention, processus est considéré comme le thread principal. + +## fin d'un processus léger + +Un thread se termine : + + - lorsque sa fonction principale se termine + - lorsqu'il est annulé avec l'appel de `pthread_cancel()` + - lorsque l'un des thread appelle `exit()`, dans ce cas tous les processus + légers associés se terminent. + - lorsque le thread appelle `pthread_exit()` : + ```c + #include + void pthread_exit(void *retval); + ``` + Cette fonction ne retourne rien à l'appelant, `retval` sera renvoyé aux + autres processus en attentes de sa terminaison. + +## Attendre la fin d'un thread + +```c +#include +int pthread_join(pthread_t thread, void **retval) +``` + +Cette fonction renvoie 0 en cas de succès, sinon `errno` est positionné. C'est +un appel bloquant, le processus appelant sera bloqué jusqu'à la terminaison du +thread attendu. `retval` prendra la valeur de retour du processus léger attendu. + +## Exemple : hello world avec un thread + +```c +#include +#include +#include +#include +void *tmain(void *arg) { + char *msg = (char *) arg; + printf("[T] %s", msg) ; + pthread_exit((void *) strlen(msg)); +} +int main(int argc, char **argv) { + pthread_t t; + int r; + void *res; + printf("Creating thread...\n"); + r = pthread_create(&t, NULL, tmain, (void *) "Hello World!\n"); + if (r != 0) { + perror("Unable to create thread"); + exit(EXIT_FAILURE); + } + r = pthread_join(t, &res); + if (r != 0) { + perror("Unable to join thread"); + exit(EXIT_FAILURE); + } + printf("Thread return: %ld\n", (long) res); + exit(EXIT_SUCCESS); +} +``` + +## Processus légers zombies + +Tout comme un processus léger normal, un processus léger zombie est un processus +léger dont aucun autre n'a pris connaissance de sa fin. Les threads zombies +posent les mêmes problèmes de consommation ressources que les processus. + +Sur certains systèmes UNIX, le nombres de processus légers par processus est +limité au niveau du noyau : + +```shell +ephase@archlinux$ cat /proc/sys/kernel/threads-max +125626 +``` + +## Détacher un processus léger + +Les processus légers détachés ne sont plus joignables et seront pris en compte +automatiquement à leurs terminaison (sans devenir zombie). + +```c +#include +int pthread_detach(pthread_t thread); +``` + +Retourne 0 en cas de succès, sinon `errno` est positionnée. + +## Identité d'un processus léger + +Les identifiants de threads doivent être considéré comme opaque. Il n'existe pas +de façons portables de comparer directement deux valeurs de `pthread_t` + +```c +#include + +pthread_t pthread_self(void); +// Retourne l'identifiant du thread appelant +// Cette fonction réussit toujours + +int pthread_equal(pthread_t t1, pthread_t t2); +// Retourne une valeur non nulle si les 2 +// identifiants sont égaux +// Retourne 0 dans le cas contraire +``` + +## Les section critiques + +### définition + +Une section critique est un fragment de code qui accède à un ressource partagées +(variable globale par exemple) et devant être exécuter de façon atomique au +regard des autres entités qui veulent y accéder. + +Une section critique peut être protégée par un **mutex**, un **sémaphore** ou +d'autres primitives de programmation concurrente. + +Voir [section critique][l_seccrit] sur Wikipedia + +[l_seccrit]:https://fr.wikipedia.org/wiki/Section_critique + + +#### Atomicité + +Opération ou ensemble d'opération devant s'exécuter entièrement sans pouvoir +être interrompues. + +Voir [atomicité][l_atomicite] sur Wikipedia + +[l_atomicité]:https://fr.wikipedia.org/wiki/Atomicit%C3%A9_(informatique) + +### Exemple + +On souhaite incrémenter une variable global`static long glob = 0;` depuis deux +(ou plus) threads qui s’exécute simultanément. + +#### Code C + +```C +#include +#include +#include +#include + +#define LOOP_COUNT 100000000 +#define THREADS_COUNT 2 + +static long glob = 0; + +void *tmain(void *arg) { + long loc; + for(long i = 0; i< LOOP_COUNT; i++) { + loc = glob; + loc++; + glob = loc; + } + return NULL; +} +int main(int argc, char **argv) { + pthread_t tlist[THREADS_COUNT]; + int r; + void *res; + for(int i=0; i T1 possède R1 mais essaye d'accéder à R2 possédée par T2 qui essaye d'accéder +> à R1 + +### Les cas limites + + 1. Un thread tente de déverrouiller un mutex pour la seconde fois + 2. Un thread tente de déverrouiller un mutex qui n'est pas verrouillé. + 3. Un thread qui tente de déverrouiller un mutex qu'il n'as pas verrouillé + lui-même + +### Les différents types de mutex + + - PTHREAD_MUTEX_NORMAL : il n'y a pas de détection d'interblocage avec ce type + de mutex, seul le cas limite 1 finira en erreur. + + - PTHREAD_MUTEX_ERRORCHECK : détection complète de l'interblocage, ainsi les 3 + cas limites cités plus haut retournerons une erreur. C'est cependant plus + lent au niveau de l'exécution. + + - PTHREAD_MUTEX_RECURSIVE : les verrouillages multiples sur les mutex peuvent + sont possibles mais requièrent le même nombre de déverrouillages. Les cas + limite # retournera cependant une erreur. + +## Les variables conditions + +Les mutex empêchent l'accès à une section critique par plusieurs threads, mais +l'utilisation de boucles d'attentes induites par leurs utilisations entraîne +une utilisation intensive du CPU. Il est donc possible pour un thread d'utiliser +des **variables conditions** afin d'informer ses pairs d'une changement d'une +ressource partagée. Ainsi les autres threads peuvent se mettre en attente +jusqu'à obtention du changement d'état de la ressource demandée. + +Il existe deux états pour une variable condition : `wait` (bloque jusqu'à +l'arrivée d'une notification) et `signal` (notification). Une variable condition +est *sans état* : si une notification est envoyée mais qu'aucun thread ne +l'attend, elle est perdue. + +### Déclaration + +Tout comme les mutex, il existe deux façon de déclarer des variables condition : +statique ou dynamique (sur le tas) : + +```C +// statique +pthread_cond_t cond = PTHREAD_COND_INITIALIZER ; + +// dynamique +int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); +``` + +La version dynamique renvoie 0 en cas de succès, sinon `errno` est positionnée. + +### Mise en attente d'un thread + +```C +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); +``` + +Bloque le thread appelant jusqu'à l'arrivée d'une notification. L'appel de cette +fonction déverrouille atomiquement le mutex quoi doit être verrouillé par le +thread appelant avant. Lorsque `pthread_cond_wait` rend la main au thread +appelant, il reverrouille le mutex comme `pthread_lock_mutex` + +Comme pour les mutex, la fonction envoie 0 en cas de succès et positionne +`errno` dans le cas contraire. + +### Envoi d'une notification + +```C +int pthread_cond_signal(pthread_cond_t *cond); +int pthread_cond_broadcast(pthread_cond_t *cond); +``` + +`pthread_cond_signal` relance l'un de thread mis en attente par +`pthread_cond_wait` avec comme variable de signal `cond`. si plusieurs threads +attendent, seul l'un d'eux sera relancé mais impossible de savoir lequel. Si +aucun thread attend, rien ne se passe + +`pthread_cond_broadcast` relance tous les threads mis en attente avec comme +variable de signal `cond`. si aucun thread n'est en attente, rien ne se passe. + +### Détruire une variable condition. + +Les variables conditions allouées dynamiquement doivent être détruites une fois +qu'elle n'ont plus d'utilité. + +```C +in pthread_cond_destroy(pthread_cond_t *cond); +``` + +Une variable condition ne doit être détruite seulement si aucun thread n'est en +attente sur cette même condition. Comme d'habitude, retourne 0 en cas de succès +et positionne `errno` en cas d'erreur. + +### Exemple de code C avec mutex et variable condition + +```C +#include +#include +#include +#include + +static long goods = 0; +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t avail = PTHREAD_COND_INITIALIZER; + +void *producer(void *arg) { + while(1) { + sleep(1); // simulate time to produce + if (pthread_mutex_lock(&mutex) != 0) { + perror("Mutex lock failed"); + exit(EXIT_FAILURE); + } + goods++; + if (pthread_mutex_unlock(&mutex) != 0) { + perror("Mutex unlock failed"); + exit(EXIT_FAILURE); + } + if (pthread_cond_signal(&avail) != 0) { + perror("Condition signal failed"); + exit(EXIT_FAILURE); + } + } + return NULL; +} + +void *consumer(void *arg) { + while(1) { + if (pthread_mutex_lock(&mutex) != 0) { + perror("Mutex lock failed"); + exit(EXIT_FAILURE); + } + while(goods == 0) { + if (pthread_cond_wait(&avail, &mutex) != 0) { + perror("Condition wait failed"); + exit(EXIT_FAILURE); + } + } + while(goods > 0) { + goods--; + printf("Consuming...\n"); + } + if (pthread_mutex_unlock(&mutex) != 0) { + perror("Mutex unlock failed"); + exit(EXIT_FAILURE); + } + } + return NULL; +} + +int main(int argc, char **argv) { + pthread_t t_producer, t_consumer; + printf("Creating producer thread...\n"); + if (pthread_create(&t_producer, NULL, producer, NULL) != 0) { + perror("Unable to create producer thread"); + exit(EXIT_FAILURE); + } + printf("Creating consumer thread...\n"); + if (pthread_create(&t_consumer, NULL, consumer, NULL) != 0) { + perror("Unable to create consumer thread"); + exit(EXIT_FAILURE); + } + if (pthread_join(t_producer, NULL) != 0) { + perror("Unable to join producer thread"); + exit(EXIT_FAILURE); + } + if (pthread_join(t_consumer, NULL) != 0) { + perror("Unable to join consumer thread"); + exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); +} +``` + +## Les fonctions *Threads Safe* + +On dit d'une fonction quell est *threads safe*lorsqu'elle est capable de +fonctionner correctement lorsqu'elle est exécutée simultanément au sin d'un même +espace d'adressage par plusieurs threads. + +Les threads ne devraient pas invoquer de fonction non thread safe sans mettre +en place des mécanisme de synchronisation entre eux comme les mutex. + +## Bibliographie + + - [Posix Thread Programming][l_posixlprog] sur le site du Livemore Computing + Center (en) + - [Initiation à la programmation multitâches en C avec pthreads][l_progcmulti] + par Franck Hecht sur Developpez.com. Avec un test de performance sur un + programme C avec et sans variable condition. + +[l_posixtprog]:https://computing.llnl.gov/tutorials/pthreads/ +[l_progcmulti]:https://franckh.developpez.com/tutoriels/posix/pthreads/