# CHAPITRE : GESTION DES PROCESSUS - PARTIE 1 -

# I - Introduction

Les années 1960 ont constitué un tournant pour les système d'exploitation. Dans le cadre du projet MAC au MIT (Massachusset Institute of Technology), le système d'exploitation CTTS est publié en 1961. C'est l'un des tout premiers système d'exploitation à temps partagé, permettant à plusieurs utilisateurs d'utiliser un ordinateur en même temps. C'est une déclinaison du multitâche apparu peu de temps avant, et c'est cette apparente simultaneité dans l'exécution des programmes qui a permis de se diriger vers l'informatique moderne.

Dans les années 1970 les ordinateurs personnels n'étaient pas capables d'exécuter plusieurs tâches à la fois : on lancait un programme et on y restait jusqu'à ce que celui-ci plante ou se termine. Les systèmes d'exploitation récents (Windows, Linux ou osX par exemple) permettent d'exécuter plusieurs tâches simultanément - ou en tous cas, donner l'impression que celles-ci s'exécutent en même temps. A un instant donné, il n'y a donc pas un mais plusieurs programmes qui sont en cours d'exécution sur un ordinateur : on les nomme processus. Une des tâches du système d'exploitation est d'allouer à chacun des processus les ressources dont il a besoin en termes de mémoire, entrées-sorties ou temps processeur, et de s'assurer que les processus ne se gênent pas les uns les autres.

si l'on prend l'exemple du smartphone, cet ordinateur que l'on a dans la poche, alors que nous regardons une vidéo, il va suivre les antennes relais et se synchroniser avec, écouter s'il y a un appel téléphonique ou des SMS qui arrive, vérifier les nouveaux courriers électroniques, mettre à jour les notifications des différents réseaux sociaux,etc. Il y a toute une myriade de processus qui s'éxécute de façon «simultanée».

Nous avons tous été confrontés à la problématique de la gestion des processus dans un système d'exploitation, en tant qu'utilisateur :

- quand nous cliquons sur l'icône d'un programme, nous provoquons la naissance d'un ou plusieurs processus liés au programme que nous lançons
- quand un programme ne répond plus, il nous arrive de lancer le gestionnaire de taches pour tuer le processus en défaut

Nous allons voir en détails dans cette séquence comment les processus sont gérés dans le système d'exploitation Linux.

# II - Qu'est-ce qu'un processus ?

Un processus est un programme en cours d'exécution sur un ordinateur. Il est caractérisé par :

- un ensemble d'instructions à exécuter - souvent stockées dans un fichier sur lequel on clique pour lancer un programme (par exemple firefox.exe)
- un espace mémoire dédié à ce processus pour lui permettre de travailler sur des données qui lui sont propres : si vous lancez deux instances de firefox, chacune travaillera indépendament de l'autre avec ses propres données.
- des ressources matérielles : processeur, entrées-sorties (accès à internet en utilisant la connexion Wifi).
- des ressources mémoires

Il ne faut donc pas confondre le fichier contenant un programme (portent souvent l'extension .exe sous windows) et le ou les processus qu'il engendre quand il est exécuté : Un programme est juste un fichier contenant une suite d'instructions (firefox.exe par exemple) alors que les processus sont des instances de ce programme ainsi que les ressources nécessaires à leur exécution (plusieurs fenêtres de firefox ouvertes en même temps).

Pour prendre une image assez classique, si une recette de cuisine correspond au code source du programme, les tâches du cuisinier en train de préparer cette recette dans sa cuisine correspondent aux processus.

## Création d'un processus

La création d'un processus peut intervenir

- au démarrage du système
- par un appel d'un autre processus
- par une action d'un utilisateur (lancement d'application)

Un processus peut créer un ou plusieurs processus à l'aide d'une commande système ("fork" sous les systèmes de type Unix). Imaginons un processus A qui crée un processus B. On dira que A est le père de B et que B est le fils de A. B peut, à son tour créer un processus C (B sera le père de C et C le fils de B). On peut modéliser ces relations père/fils par une structure arborescente.

Si un processus est créé à partir d'un autre processus, comment est créé le tout premier processus ?
Sous un système d'exploitation comme Linux, au moment du démarrage de l'ordinateur un tout premier processus (appelé processus 0 ou encore Swapper est créé à partir de "rien" (il n'est le fils d'aucun processus). Ensuite, ce processus 0 crée un processus souvent appelé "init" ("init" est donc le fils du processus 0). À partir de "init", les processus nécessaires au bon fonctionnement du système sont créés (par exemple les processus "crond", "inetd", "getty",...) Puis d'autres processus sont créés à partir des fils de "init"...

Ouvrir le Simulateur terminal linux aussi apellé shell >

Dans un terminal, tester l'instruction `pstree -p` qui permet de visualiser l'arbre de processus. Recopier ci-dessous, l'arbre des processus :

blabla

## Gérer les processus sur un système Linux: PID et PPID

Il est possible de visualiser les processus grâce à la commande `ps -eF`.
Pour un affichage page par page, utilisez `ps -eF | more`

Un processus est caractérisé par un identifiant unique : son PID (Process Identifier).
Lorsqu'un processus engendre un fils, l'OS génère un nouveau numéro de processus pour le fils. Le fils connaît aussi le numéro de son père : le PPID (Parent Process Identifier).

1. Quel est le PID du processus init ?
2. Quel est le PPID de init ?
3. init possède t-il un frêre ?
4. Citer quelques descendants directs de init

1. ...
2. ...
3. ...
4. ...

1. Dans le terminal taper `python`
2. Faire un petit calcul puis stopper python avec les touches CTRL + Z
3. Observer que le processus python est encore actif puis donner son PID
4. Recopier ci-dessous, l'arbre des processus.

1. ...
2. ...
3. ...
4. ...

## Inspecter les processus en temps réel

Une commande indispensable à connaître sous Linux pour inspecter les processus est la commande `top`.

L'affichage se rafraîchit en temps réel contrairement à `ps` qui fait un instantané.
Voici quelques option qui s'activent s'activent par des raccourcis clavier. En voici quelques uns :

- h : affiche l'aide
- M : trie la liste par ordre décroissant d'occupation mémoire. Pratique pour repérer les processus trop gourmands
- P : trie la liste par ordre décroissant d'occupation processeur
- i : filtre les processus inactifs. Cela ne montre que ceux qui travaillent réellement.
- k : permet de tuer un processus - à condition d'en être le propriétaire.
- V : permet d'avoir la vue arborescente sur les processus.
- q : permet de quitter `top`

1. Tester la commande `top`.
2. Quel est le processus le plus gourmand ?
3. Essayer de tuer le processus init.
4. Tuer le processus python .
5. Tuer le processus top .

blabla

## Terminer un processus

Pour tuer un processus, on lui envoie un signal de terminaison. On en utilise principalement deux :

- SIGTERM (15) : demande la terminaison d'un processus. Cela permet au processus de se terminer proprement en libérant les ressources allouées.
- SIGKILL (9) : demande la terminaison immédiate et inconditionnelle d'un processus. C'est une terminaison violente à n'appliquer que sur les processus récalcitrants qui ne répondent pas au signal SIGTERM.

Pour terminer proprement un processus :
vous lui enverrez donc un signal SIGTERM en tapant : la commande shell `kill -15 PID` où PID désigne le numéro du processus à quitter proprement.

Si ce dernier est planté et ne réagit pas à ce signal, alors vous pouvez vous en débarasser en tapant `kill -9 PID`.

1. Redemarrer le processus python
2. Donner son PID
3. Tuez le processus python avec la commande `kill`

# Gestion des processus et des ressources

## Exécution concurrente

Les systèmes d'exploitation modernes sont capable d'exécuter plusieurs processus "en même temps". En réalité ces processus ne sont pas toujours exécutés "en même temps" mais plutôt "à tour de rôle". On parle d'exécution concurrente car les processus sont en concurrence pour obtenir l'accès au processeur chargé de les exécuter.

> Sur un système multiprocesseur, il est possible d'exécuter de manière parallèle plusieurs processus, autant qu'il y a de processeurs. Mais sur un même processeur, un seul processus ne peut être exécuté à la fois.

On peut voir assez facilement cette exécution concurrente. Considérons les deux programmes Python suivants :

`progA.py`

    import time

    for i in range(100):
        print("programme A en cours, itération", i)
        time.sleep(0.01)  # pour simuler un traitement avec des calculs

Python

`progB.py`

    import time

    for i in range(100):
        print("programme B en cours, itération", i)
        time.sleep(0.01)  # pour simuler un traitement avec des calculs

Python

En ouvrant un Terminal, on peut lancer simultanément ces deux programmes avec la commande

    python3 progA.py & python3 progB.py &

> Le caractère `&` qui suit une commande permet de lancer l'exécution en arrière plan et de rendre la main au terminal.

Le shell indique alors dans la console les PID des processus correspondant à l'exécution de ces deux programmes (ici 9154 et 9155) puis on constate grâce aux affichages que le système d'exploitation alloue le processeur aux deux programmes à tour de rôle

## Accès concurrents aux ressources

Une ressource est une entité dont a besoin un processus pour s'exécuter. Les ressources peuvent être matérielles (processeur, mémoire, périphériques d'entrée/sortie, ...) mais aussi logicielles (variables).

Les différents processus se partagent les ressources, on parle alors d'accès concurrents aux ressources. Par exemple,

- les processus se partagent tous l'accès à la ressource "processeur"
- un traitement de texte et un IDE Python se partagent la ressource "clavier" ou encore la ressource "disque dur" (si on enregistre les fichiers), ...
- un navigateur et un logiciel de musique se partagent la ressource "carte son", ...

C'est le système d'exploitation qui est chargé de gérer les processus et les ressources qui leur sont nécessaires, en partageant leur accès au processeur. Nous allons voir comment tout de suite !

## États d'un processus

Tous les systèmes d'exploitation "modernes" (Linux, Windows, macOS, Android, iOS...) sont capables de gérer l'exécution de plusieurs processus en même temps. Mais pour être précis, cela n'est pas en véritable "en même temps", mais plutôt un "chacun son tour".

Un système d’exploitation multitâche ré-attribue périodiquement à l’UC une tâche différente dans le but de faire progresser l’exécution de plusieurs programmes à la fois.

- La notion d’ordonnancement de tâches est alors apparue.
- L’utilisateur a l’impression que plusieurs programmes sont exécutés « simultanément ».

A contrario, un système d’exploitation monotâche exécute une commande uniquement lorsque la précédente est terminée. C'est le principe du multitâche qui rend nos outils informatiques si réactifs et intuitifs. Au niveau du noyau, l'ordonnanceur est chargé de gérer la répartition du temps de processeur entre les différentes tâches. Il est une des pièces du complexe processus de gestion des processus.
Pour gérer ce "chacun son tour", les systèmes d'exploitation attributs des "états" au processus.

Voici les différents états :

- Lorqu'un processus est en train de s'exécuter (qu'il utilise le microprocesseur), on dit que le processus est dans l'état "élu".
- Un processus qui se trouve dans l'état élu peut demander à accéder à une ressource pas forcément disponible instantanément (par exemple lire une donnée sur le disque dur). Le processus ne peut pas poursuivre son exécution tant qu'il n'a pas obtenu cette ressource. En attendant de recevoir cette ressource, il passe de l'état "élu" à l'état "bloqué"
- Lorsque le processus finit par obtenir la ressource attendue, celui-ci peut potentiellement reprendre son exécution. Mais comme nous l'avons vu ci-dessus, les systèmes d'exploitation permettent de gérer plusieurs processus "en même temps", mais un seul processus peut se trouver dans un état "élu" (un coeur de processseur ne peut "s'occuper" que d'un seul processus à la fois). Quand un processus passe d'un état "élu" à un état "bloqué", un autre processus peut alors "prendre sa place" et passer dans l'état "élu". Le processus qui vient de recevoir la ressource attendue ne va donc pas forcément pouvoir reprendre son exécution tout de suite, car pendant qu'il était dans à état "bloqué" un autre processus a "pris sa place". Un processus qui quitte l'état bloqué ne repasse pas forcément à l'état "élu", il peut, en attendant que "la place se libère" passer dans l'état "prêt" (sous entendu "j'ai obtenu ce que j'attendais, je suis prêt à reprendre mon exécution dès que la "place sera libérée"").

Le passage de l'état "prêt" vers l'état "élu" constitue l'opération "d'élection".
Le passage de l'état élu vers l'état bloqué est l'opération de "blocage".
Pour se terminer, un processus doit obligatoirement se trouver dans l'état "élu".

On peut résumer tout cela avec le diagramme suivant :

Lorsqu'un processus est interrompu, il doit pouvoir reprendre à l'endroit même où il a été interrompu. Pour cela, le système d'exploitation conserve pour chaque processus créé une zone mémoire (appelée PCB, pour Process Control Bloc, ou bloc de contrôle du processus) dans laquelle sont stockées les informations sur le processus : son PID, son état, la valeur des registres lors de sa dernière interruption, la zone mémoire allouée par le processus lors de son exécution, les ressources utilisées par le processus (fichiers ouverts, connexions réseaux en cours d'utilisation, etc.).

Il est vraiment important de bien comprendre que le "chef d'orchestre" qui attribue aux processus leur état "élu", "bloqué" ou "prêt" est le système d'exploitation .
On dit que le système gère l'ordonnancement des processus (tel processus sera prioritaire sur tel autre...)

Chose aussi à ne pas perdre de vu : Pour libérer une ressource, un processus doit obligatoirement être dans un état "élu".

Afin d'élire quel processus va repasser en mode "élu", l'ordonnanceur applique un algorithme prédéfini lors de la conception de l'OS.
Le choix de cet algorithme va impacter directement la réactivité du système et les usages qui pourront en être fait.
C'est un élément critique du système d'exploitation.
Sous Linux, on peut passer des consignes à l'ordonnanceur en fixant des priorités aux processus dont on est propriétaire :
Cette priorité est un nombre NI entre -20 (plus prioritaire) et +20 (moins prioritaire).

Pour gérer l'élection d'un processus,on peut agir à 2 niveaux :

- fixer une priorité à une nouvelle tache dès son démarrage avec la commande `nice`
- modifier la priorité d'un processus en cours d'exécution grâce à la commande `renice`

les colonne PR et NI de la commande `top` montrent le niveau de priorité de chaque processus

Le lien entre PR et NI est simple : PR = NI + 20 ce qui fait qu'une priorité PR de 0 équivaut à un niveau de priorité maximal.

Exemple : Pour baisser la priorité du process `terminator` dont le PID est 21523, il suffit de taper : `renice +10 21523`

1. Changer la priorité du processus python
2. Pour tous les processus, vérifier que PR = NI + 20
3. Mettre une priorité NI à 30 pour le processus python
4. Dire à quel intervalle appartinet NI ?
5. Dire à quel intervalle appartinet PR ?
6. On souhaite mettre une priorité maximal à python, quel NI mettre ?

1. ...
2. ...
3. ...
4. ...
5. ...
6. ...

Plus précisément, c'est l'ordonnanceur (un des composants du système d'exploitation) qui réalise cette tâche appelée ordonnancement des processus.

L'objectif de l'ordonnanceur est de choisir le processus à exécuter à l'instant t

## Exemple d'ordonnancement

C'est le système d'exploitation qui attribue aux processus leurs états élu, prêt et bloqué. Plus précisément, c'est l'ordonnanceur (un des composants du système d'exploitation) qui réalise cette tâche appelée ordonnancement des processus.

L'objectif de l'ordonnanceur est de choisir le processus à exécuter à l'instant t (le processus élu) et déterminer le temps durant lequel le processeur lui sera alloué.

Fig. 2 - Ordonnancement des processus.

Ce choix est à faire parmi tous les processus qui sont dans l'état prêt, mais lequel sera élu ? et pour combien de temps ? Des algorithmes d'ordonnancement sont utilisés et il en existe plusieurs selon la stratégie utilisée. On en présente quelques-uns ci-dessous.

### Ordonnancement First Come First Served (FCFS)

Principe : Les processus sont ordonnancés selon leur ordre d'arrivée ("premier arrivé, premier servi" en français)

Exemple : Les processus P1(53), P2(17), P3(68) et P4(24) arrivent dans cet ordre à t=0.

> P1(53), P2(17), P3(68) et P4(24) signifie que P1 , P2, P3 et P4 ont besoin de respectivement 53, 17, 68 et 24 unités de temps pour s'exécuter.

### Ordonnancement Shortest Job First (SJF)

Principe : Le processus dont le temps d'exécution est le plus court est ordonnancé en premier.

Exemple : P1, P2, P3 et P4 arrivent à t=0 :

### Ordonnancement Shortest Remaining Time (SRT)

Principe : Le processus dont le temps d'exécution restant est le plus court parmi ceux qui restent à exécuter est ordonnancé en premier.

Exemple : P3 et P4 arrivent à t=0 ; P2 à t=20 ; P1 à t=50 :

### Ordonnancement temps-partagé (Round-Robin)

Principe : C'est la politique du tourniquet : allocation du processeur par tranche (= quantum q) de temps.

p> Exemple : quantum q=20 et n=4 processus

Dans ce cas, s'il y a unités de temps au plus

### Ordonnancement à priorités statiques

Principe : Allocation du processeur selon des priorités statiques (= numéros affectés aux processus pour toute la vie de l'application)

Exemple : priorités où la priorité la plus forte est 0 (attention, dans certains systèmes c'est l'inverse : 0 est alors la priorité la plus faible)

# IV- Les threads

Nous avons vu comment une hiérarchie de processus se crée au fur et à mesure de l'utilisation de l'ordinateur, tous ayant pour racine le processus initial (systemd ou init sur les distributions linux).

### Programmation séquentielle

Commençons par un programme simple :

    #!/usr/bin/python3
    def f1():
      for _ in range(5):
        print("Bonjour !")
        print("Comment vas-tu aujourd'hui ?")
        sleep(0.01)

    def f2():
      for _ in range(5):
        print("Ça va ?")
    	print()
        sleep(0.01)

    f1()
    f2()

1. Recopiez le code ci dessus dans un fichier que vous nommerez `sequentiel.py` et exécutez le.
2. Chronométrer le temps mis pour executer 100 fois le programme ci-dessus.

Chronométre : .....

Lorsque vous exécutez le programme, vous avez la fonction `f1` qui s'éxécute complétement, puis ensuite la fonction `f2` qui s'éxécute.
L'excecution des fonctions est dite séquentielle .

### Nos premiers threads

Un thread est un processus qui va partager avec notre programme l'espace des données et va s'éxécuter de façon simultané avec d'autres thread. On parle de processus léger. Ils peuvent être très utile, mais peuvent aussi causer de multiples problèmes. Mettons les choses en pratique. Pour cela, nous allons utiliser la bibliothèque threading avec le code ci dessous :

    #!/usr/bin/python3
    from threading import Thread
    from time import sleep

    def f1():
      for _ in range(5):
        print("Bonjour !")
        print("Comment vas-tu aujourd'hui ?")
        sleep(0.01)

    def f2():
      for _ in range(5):
        print("Très bien")
        print("Merci et bonne journée.")
        sleep(0.01)

    p1 = Thread(target=f1)
    p2 = Thread(target=f2)
    p1.start()
    p2.start()
    p1.join()
    p2.join()

1. Recopiez le code ci dessus dans un fichier que vous nommerez `concurrente.py` et exécutez le, plusieurs fois de préférence. Que remarquez vous ?
2. Chronométrer le temps mis pour executer 100 fois le programme ci-dessus.
3. Comparer avec la méthode séquentielle.

2. ....
3. ...

### Comment ça marche ?

    from threading import Thread
    from time import sleep

Les deux premières lignes du programme se contentent d'importer la classe `Thread` du module threading et la fonction `sleep` du module time (nous avons besoin de ralentir le processus avec sleep sinon la première fonction s'éxécuterait trop vite et afficherait tout d'un coup).

Les fonctions `f1` et `f2` sont exactement les même que dans la version séquentielle. Mais c'est dans la partie principale du programme que tout change.

    p1 = Thread(target=f1)
    p2 = Thread(target=f2)
    p1.start()
    p2.start()
    p1.join()
    p2.join()

Ici, nous n'éxécutons pas directement les fonctions `f1` et `f2`. Nous créons deux objets de la classe `Thread`. Ce sont des processus légers qui vont partager l'espace mémoire de notre programme principal et s'éxécuter de façon parallèle. Les deux lignes suivantes appellent la méthode `start` sur les Thread, et va lancer leur exécution. Mais pendant que le premier s'éxécute, le programme continue et va lancer le second.

Enfin, nous utilisons la méthode `join` sur ces deux threads. En effet, le programme principal continue de s'exécuter pendant que les threads tournent, et si il se termine, il met fin à tous ses threads. La méthode `join` force le programme principal à attendre la fin des threads.

#### Quel est l'avantage ?

L'avantage principal, c'est de pouvoir faire plusieurs choses en même temps. Surtout si on travaille sur une machine qui a plusieurs processeur. Par exemple, on peut avoir une machine qui a deux processeurs, chaque processeur ayant quatre cœurs, chaque cœeurs pouvant lui même exécuter deux threads. on va pouvoir au total exécuter en parallèle 16 threads !

### Calculons plus vite (ou comment les ennuis commencent)

Imaginons que j'ai un programme qui doit bêtement faire 400 calculs. On va simuler cela par un petit `sleep` avec le code suivant

    #!/usr/bin/python3
    from time import sleep

    # Variable globale
    compteur = 0
    limite = 400

    def calcul():
      global compteur
      for c in range(limite):
        temp = compteur
        # simule un traitement nécessitant des calculs
        sleep(0.000000001)
        compteur = temp + 1

    compteur = 0
    calcul()
    print(compteur)

1. Recopiez le code ci dessus dans un fichier que vous nommerez `calcul.py` et exécutez le. Vérifiez qu'il affiche bien 400.

Je sais que je suis sur une machine qui peut exécuter 8 threads de façon parallèle et je me dis qu'il serait plus rapide de faire 4 thread différents qui font chacun un quart des calculs. En faisant ça, le résultat devrait être donné environ 4 fois plus vite. Traduisons cela avec du code :

    #!/usr/bin/python3
    from threading import Thread
    from time import sleep

    # Variable globale
    compteur = 0
    limite = 100

    def calcul():
      global compteur
      for c in range(limite):
    	temp = compteur
    	# simule un traitement nécessitant des calculs
    	sleep(0.000000001)
    	compteur = temp + 1

    compteur = 0
    mesThreads = []
    for i in range(4): # Lance en parallèle 4 exécutions de calcul
      p = Thread(target = calcul)
      p.start()      # Lance calcul dans un processus léger à part.
      mesThreads.append(p)

    # On attend la fin de l'exécution des threads.
    for p in mesThreads :
    	p.join()

    print(compteur)

Recopiez le code ci dessus dans un fichier que vous nommerez `calcul-concurrent.py` et exécutez le, de préférence plusieurs fois.

Ah. Le résultat n'est pas du tout le résultat attendu. Chaque thread se lance et fait 100 calcul, mais mon compteur à la fin ne vaut en général même pas 100 ! Ce résultat est très perturbant quand on le rencontre pour la première fois, et cela explique la réticence de bien des développeurs à l'égard des threads. On peut lire sur bien des forums de développeur «Threads are EVIL, don't use them !». Mais non, il ne sont pas le diable, il faut juste être parfaitement conscient de ce qui se passe. Ils sont même indispensable dans bien des programmes, particulièrment tous les programmes client/serveur qui doivent répondre à un grand nombre de requêtes concurrentes.

#### Que s'est il passé ?

Il n'y a rien d'illogique ou d'aléatoire dans le fonctionnement de notre programme. Il faut simplement habituer notre esprit à l'exécution en parallèle :

- quatre processus (appelons les P1, P2, P3 et P4) exécutent la fonction calcul simultanément. Celle-ci utilise une variable globale qui sera donc modifiée par chacun de ces processus et une variable locale temp qui sera spécifique à chacun de nos processus. Nous la désignerons par temp(P1) temp(P2) etc... Un scénario possible est le suivant : Imaginons au départ que compteur vaille 10.
- P1 sauvegarde compteur dans temp(P1) --> temp(P1) vaut 10
- P2 sauvegarde compteur dans temp(P2) --> temp(P2) vaut 10
- P3 sauvegarde compteur dans temp(P3) --> temp(P3) vaut 10
- P1 et P2 incrémentent temp et sauvegardent la réponse dans compteur --> compteur vaut 11
- P4 sauvegarde compteur dans temp(P4) --> temp(P4) vaut 11
- P3 et P4 incrémentent temp et sauvegardent la réponse dans compteur qui vaut donc maintenant 12

au final, compteur a été incrémenté 4 fois mais de fait de l'exécution en parallèle compteur ne vaut pas 14 mais 12 ! cela explique que notre compteur au final ne vaut pas 400 car sa sauvegarde dans des variables temporaires fait que la plupart des incrémentations ne sont pas prises en compte.

Le résultat est aléatoire par ce que les threads s'exécutent dans un ordre qui peut varier, comme nous l'avons vu sur l'exemple des salutations. C'est le principal problème avec les threads : on ne maîtrise absolument pas l'ordre dans lequel ils sont exécutés, et il faut en tenir compte dès la conception.

#### Peut on résoudre notre problème ?

Vous vous en doutez, la réponse est oui. Comment ? Il existe un mécanisme qui va nous ralentir potentiellement un peu mais qui évite ce genre de problèmes : les verrous. Ce sont des objets de la classe `Lock` du module threading. Dans notre cas, ils ont deux méthodes qui nous intéressent.

- La méthode `acquire` s'accapare le verrou s'il est disponible, sinon elle attend qu'il se libère.
- La méthode `release` libère le verrou.

Ce verrou sera une variable globale du programme principal qui sera partagé entre les threads. Notre programme devient alors le suivant :

    #!/usr/bin/python3
    from threading import Thread,Lock
    from time import sleep

    # Variable globale
    compteur = 0
    limite = 100
    verrou = Lock()

    def calcul():
      global compteur
      for c in range(limite):
    	# Début de la section critique
    	verrou.acquire()
    	temp = compteur
    	# simule un traitement nécessitant des calculs
    	sleep(0.000000001)
    	compteur = temp + 1
    	# fin de la section critique
    	verrou.release()

    compteur = 0
    mesThreads = []
    for i in range(4): # Lance en parallèle 4 exécutions de calcul
      p = Thread(target = calcul)
      p.start()      # Lance calcul dans un processus léger à part.
      mesThreads.append(p)

    # On attend la fin de l'exécution des threads.
    for p in mesThreads :
    	p.join()

    print(compteur)

1. Recopiez le code ci dessus dans un fichier que vous nommerez `verrou.py` et exécutez le. Vérifiez que cette fois le résultat est bien 400.
2. L'utilisation de tread avec la mise en plavce des verrous permet-il un gain de temps ?

(sans tread) Chronometre : ....
(avec tread et sans verrous ) Chronometre : ....
(avec tread et AVEC verrous ) Chronometre : ....
Conclusion :

Alors ? Nos problèmes sont résolus ? En fait, non, ils ne font que commencer comme on va le voir dans la partie suivante sur l'interblocage.

# V- Interblocage (deadlock)

Lire la page

# Je teste mes connaissances ...

1. Quelle est la différence entre un programme et un processus ?
2. Au sein du système d'exploitation Linux, comment sont organisés l'ensemble des processus.
3. Comment s'apelle le numéro qui permet d'identifier un processus ?
4. Quels sont les trois états que peut avoir un processus ?
5. Au cours de son excécution, un même processus peut-il passer plusieurs fois par l'état "élu" ?
6. Comment s'appelle l'action qui s'occupe de gérer la priorité des processus.
7. Expliquez ce qu'est une situation de deadlock.
8. Qui gére l'attribution des états d'un processsus?

Soient deux processus A, B et deux ressources R et S. Les processus utilisent les ressources
de la façon suivante :

A | B
demande R | demande S
libère R | libère S
demande S | demande R
libère S | libère R

1. A t-on une situation d'interblocage si les processus sont exécutés de façon séquentielle. (A puis B) ?
2. A t-on une situation d'interblocage si les processus sont exécutés ligne poar ligne ?

Soient deux processus A, B et deux ressources R et S. Les processus utilisent les ressources
de la façon suivante :

A | B
demande R | demande S
libère R | demande R
demande S | libère R
libère S | libère S

1. A t-on une situation d'interblocage si les processus sont exécutés de façon séquentielle. (A puis B) ?
2. A t-on une situation d'interblocage si les processus sont exécutés ligne poar ligne ?

Soient deux processus A, B et deux ressources R et S. Les processus utilisent les ressources
de la façon suivante :

A | B
demande R | demande S
demande S | libère S

1. A t-on une situation d'interblocage si les processus sont exécutés de façon séquentielle. (A puis B) ?
2. A t-on une situation d'interblocage si les processus sont exécutés ligne poar ligne ?

Considérons un système ayant sept processus, A à G, et six ressources R à W. A un instant donné, l'attribution des ressources est la suivante :

- A détient R et demande S ;
- B demande T ;
- C demande S ;
- D détient U et demande S et T ;
- E détient T et demande V;
- F détient W et demande S ;
- G détient V et demande U.

Y a-t-il un interblocage? si oui quels sont les processus concernés ?
Sources :

- https://pixees.fr/informatiquelycee/
- https://www.lecluse.fr/
- lycee.educinfo.org