Thread et Fork

by dinosaure

Comprendre ces deux notions systèmes demande à ce qu’on comprenne d’abord le Multitâche d’aujourd’hui: Le Multitâche Préemptif. Aujourd’hui, les OS comme Windows, GNU/Linux ou Mac OS X sont des systèmes préemptifs. Ils ont la capacité d’exécuter ou de stopper une tâche planifiée en cours, c’est grâce à un ordonnanceur (sheduler) préemptible qui présente l’avantage d’une meilleur réactivité du système et de son évolution. Il ne faut pas croire que vos programmes s’exécutent en même temps, c’est grâce à l’ordonnanceur qu’on génère ce qu’on appelle une simultanéité apparente qui est en faite, une alternance rapide d’exécution des processus. Et puisqu’elle est rapide, l’utilisateur lambda peut considérer que son système est multitâche.

Après, que l’ordonnanceur soit préemptible veut dire qu’il est capable de stopper un processus à tout moment pour laisser la priorité à un autre processus. Il faut aussi savoir qu’une tâche ne peut pas prendre un temps non-définie, c’est à dire qu’un temps limité est attribué au dit-processus et que si il n’accomplie pas sa tâche avant la limite fixée, la priorité revient à un autre processus. Il y a alors ce qu’on appelle la commutation de contexte où l’état de notre premier processus est enregistrer dans la mémoire. Bien sûr, cette commutation est transparente.

À l’inverse, on a ce qu’on appelle des systèmes d’exploitation collaboratifs dans lequel c’est au processus de choisir arbitrairement si il garde la main ou si il la rend. Un problème que l’on peut rencontrer dans ce genre de système et que si le processus ne redonne pas la main (parce qu’il est bugué), il peut faire arrêter le système en entier.

Fork

Le fork, c’est semblable au copier/coller mais cette fois d’un processus. Un processus A fait appelle à la fonction fork(). Il va ce copier (ainsi que toutes ces variables globales) en un processus B. Il faut cependant bien comprendre que le processus A et le processus B sont bien distinct. C’est à dire que si tout le deux contiennent la variable ma_variable, selon l’exécution du processus B, cette variable peut être différente entre les deux processus.

Néanmoins, il faut aussi signaler que les instructions du processus A sont les mêmes que celle du processus B. Alors comment on peut modifier l’état de notre variable ma_variable dans notre processus B (en sachant qu’il y a exactement les mêmes instructions) pour quelle soit différente de celle de notre processus A ? Eh bien ce qui distingue nos deux processus, c’est leurs PIDs. En effet, il nous est donc possible par condition mettant en jeu la valeur du PID du processus de faire tel ou tel instruction. Et là, il faut savoir que la fonction fork() retourne le PID du processus B dans le processus A et cette même fonction retourne 0 dans le processus B.

Au final, ce qui pourra différencier le processus A du processus B, c’est ce que retourne la fonction fork(). Néanmoins, il faut savoir que même si leurs variables à chacun sont distinctes (rappelez-vous, ma_variable est distincte dans les deux processus et elle peut être différente selon l’exécution du processus) après exécution (car à la naissance du processus B, elles sont équivoques), la position du curseur sur la sortie standard (stdout) et la même. Par contre, il n’y a pas la notion de partage et si l’un des processus ouvre un autre fichier, cela ne ce répercutera pas sur l’autre processus. Le processus B hérite des descripteurs de fichiers du processus A (dont stdout) seulement pendant le fork, c’est à dire au moment où on copie le processus A.

Ensuite, le processus A peut attendre que le processus B ait terminé pour lui aussi ce terminer, notamment grâce à la fonction wait(). Par contre, si le processus A ne contient pas l’instruction wait() (et ce serait de même pour le processus B car on copie le code), si il ce termine avant le processus B, le processus B continue ! Le processus B a donc très de dépendance au processus A et si l’un s’arrête, l’autre continue.

Thread

Un thread est similaire à un processus car il exécute aussi un ensemble d’instructions. Et comme il l’est dit en introduction, ces exécutions semble ce dérouler parallèlement (notamment grâce à la vitesse de commutation de contexte). Mais, contrairement au fork, le thread partage la même mémoire virtuelle (les variables globales ne sont plus distinctes entre processus). Par contre on créer une pile d’appel propre au thread. Ceci à pour qualité de ne pas rendre coûteux la commutation de contexte puisque la mémoire virtuelle reste la même. Par contre, si notre processus A ce termine, tout les threads de ce processus sont tués (référence à la commande kill des systèmes UNIX). Cependant, il faut signaler que le code qu’il y a dans le thread créer par son processus père n’est pas forcément le même. En effet, le thread va exécuter le code d’une fonction présente dans le processus père, c’est pour cela qu’on compare souvent le thread à un processus léger.

Le cas d’utilisation d’un thread ce fait ressentir quant il s’agit de mettre une tâche de fond à notre processus pour ne pas interrompre l’utilisation du programme par l’utilisateur. C’est le cas par exemple quant il s’agit de rechercher un élément dans le texte (ce qui peut être long selon le taille de votre texte) mais de pouvoir écrire en même temps dans le dit-texte. Le programme créer un thread qui exécutera une fonction de recherche et puisque nous utilisons des systèmes préemptifs, si l’utilisateur fait appelle au programme pour écrire dans le texte, le processus de recherche s’arrêtera. Mais vous me direz que tant qu’on écrit, la recherche ne trouvera rien, eh bien c’est là où il faut prendre en compte la rapidité de commutation de contexte et quant vous vous arrêter d’écrire pendant un petit laps de temps, le thread de recherche devient prioritaire et reprend la main. Eh oui, votre ordinateur est très rapide.

Mais venons un peu plus au plan technique. Il faut bien avoir l’idée que le thread partage la même mémoire virtuel que son père, le processus. Mais il peut y avoir collision si le processus voudrait modifier une variable globale ainsi que le thread. C’est là où intervient le notion de mutex. Le mutex doit être globale dans notre processus pour que le thread puisse aussi le voir et la capacité du mutex et de pouvoir attribuer un seuil de priorité supérieur aux autres threads et/ou au processus père. Ainsi, on peut modifier une variable sans qu’il n’y est de collision avec d’autres instructions.

On pourrait aussi parler dans le cadre de la synchronisation pour qu’il n’y est pas d’interblocages (deadlocks) sur la modification d’une variable, des sémaphores. Cette notion a été inventé par Dijkstra (un grand nom) pour résoudre le problème du dîner des philosophes qui est une représentation plus abordable pour quelqu’un qui ne fait pas de la programmation de notre problème de pouvoir modifier notre variable par le biais du processus ou d’un thread. Mais ceci est plus de l’ordre technique puisque son objectif est le même que le mutex.


Cette article essaye de bien montrer la différence entre le Fork et le Thread ce qui n’est pas très compliqué mais dont les ressources sur internet manquent. Cette article va aussi subir des updates pour rendre plus appréciable la distinction entre ces deux notions mais aussi pour faire une approche en C. En espérant que vous ayez compris cette notion !