Grâce à leur système d'exploitation multitâche, les ordinateurs exécutent plusieurs programmes de façon concurrente. L'exécution d'un programme s'appelle un processus. C'est le système d'exploitation, et en particulier l'ordonnanceur (une des fonctionnalités du noyau), qui détermine quel processus s'exécute à un instant donné. Le fait pour un processus d'être interrompu s'appelle une commutation de contexte. Plusieurs processus s'exécutant de façon concurrente peuvent s'interbloquer s'ils attendent de pouvoir accéder à un même ensemble de ressources en accès exclusif.
Les threads ou processus légers sont des "sous-processus", démarrés par un processus et s'exécutant de manière concurrente avec le reste du programme. L'accès à des ressources par plusieurs threads peut être protégé par des verrous. Une portion de code comprise entre l'acquisition et le relâchement d'un verrou s'appelle une section critique.
Le module threading de la bibliothèque standard Python permet de démarrer des threads.
Les threads (du même processus) s'exécutent dans un espace mémoire partagé, tandis que les processus s'exécutent dans des espaces mémoire différents.
Illustrer l'ordre d'exécution de threads, les problèmes de concurrence et d'interblocage.
Dans le code ci-dessous, le programme principal crée quatre threads th à l'aide de l'instruction threading.Thread(target=hello, arg=[n]). Lorsque l'on crée un thread, on lui transmet une fonction et la liste des arguments de cette fonction. La méthode start() lance l'exécution du thread en tâche de fond. Cette méthode rend la main et le programme principal continue de s'exécuter de façon concurrente au(x) thread(s) démarré(s). Une fois la boucle for exécutée, le programme comporte cinq threads : les quatre démarrés par start() plus celui associé au programme principal. Un compteur cmpt est créé dans chaque thread pour illustrer leur ordre d'exécution.
Note : la bibliothèque logging est dédiée à la journalisation.
# Programmation concurente - Illustration de l'ordre d'exécution de threads
import threading
import logging # Cette bibliothèque est dédiée à la journalisation
import time
# Fonction associée aux threads 0 à 3
def hello(num):
logging.info(f"Thread {num}: démarrage")
for i in range(5):
logging.info(f"Thread {num} : cmpt{num}={i}")
time.sleep(0.5) # Simulation d'un programme plus long
logging.info(f"Thread {num}: terminé")
# Programme principal
# Formatage des informations affichées lors du déroulement du programme
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO, # filename='thread.log', filemode='a',
datefmt="%H:%M:%S", encoding='utf-8')
for numth in range(4): # Création des threads 0 à 3
th = threading.Thread(target=hello, args=[numth]) # l'argument de type target est une fonction et l'argument
# args est un tableau d'arguments passés à la fonction.
# Ici, on passe le numéro numth du thread th à des fins d'affichage.
logging.info(f"PPrinc : avant de lancer le Thread {numth}")
th.start()
06:38:53: PPrinc : avant de lancer le Thread 0 06:38:53: Thread 0: démarrage 06:38:53: PPrinc : avant de lancer le Thread 1 06:38:53: Thread 0 : cmpt0=0 06:38:53: Thread 1: démarrage 06:38:53: PPrinc : avant de lancer le Thread 2 06:38:53: Thread 1 : cmpt1=0 06:38:53: Thread 2: démarrage 06:38:53: PPrinc : avant de lancer le Thread 3 06:38:53: Thread 2 : cmpt2=0 06:38:53: Thread 3: démarrage 06:38:53: Thread 3 : cmpt3=0 06:38:53: Thread 2 : cmpt2=1 06:38:53: Thread 1 : cmpt1=1 06:38:53: Thread 0 : cmpt0=1 06:38:53: Thread 3 : cmpt3=1 06:38:54: Thread 0 : cmpt0=2 06:38:54: Thread 1 : cmpt1=2 06:38:54: Thread 2 : cmpt2=2 06:38:54: Thread 3 : cmpt3=2 06:38:54: Thread 2 : cmpt2=3 06:38:54: Thread 1 : cmpt1=3 06:38:54: Thread 0 : cmpt0=3 06:38:54: Thread 3 : cmpt3=3 06:38:55: Thread 0 : cmpt0=4 06:38:55: Thread 1 : cmpt1=4 06:38:55: Thread 2 : cmpt2=4 06:38:55: Thread 3 : cmpt3=4 06:38:55: Thread 0: terminé 06:38:55: Thread 1: terminé 06:38:55: Thread 2: terminé 06:38:55: Thread 3: terminé
Activité 1
Exécutez plusieurs fois le code ci-dessus. Que peut-on dire de l'ordre d'exécution des threads et de l'ordre dans lequel ils s'arrêtent ?
CORRECTION Activité 1
REMARQUE : l'ordre dans lequel sont démarrés les threads ne donne aucune indication sur l'ordre dans lequel ils peuvent se terminer. |
# Programmation concurente - Compteur partagé
# Illustration du problème de concurrence v1
import threading
import logging
import time
COMPTEUR = 0 # Ressource partagée
# Fonction associée aux threads 0 à 3
def incrc(n):
global COMPTEUR
for _ in range(10):
v = COMPTEUR
logging.info(f"Thread {n} - cpt={COMPTEUR}")
COMPTEUR = v + 1
# Programme principal
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S", encoding='utf-8')
th=[] # tableau de threads
for n in range(4):
t = threading.Thread(target=incrc, args=[n])
t.start()
th.append(t)
for t in th: # Permet d'attendre que tous les threads soient terminés avant de poursuivre
t.join() # dans le programme principal
logging.info(f"Valeur finale = {COMPTEUR}") # Cette ligne est exécutée lorsque tous les threads sont terminés
06:35:52: Thread 0 - cpt=0 06:35:52: Thread 0 - cpt=1 06:35:52: Thread 1 - cpt=1 06:35:52: Thread 0 - cpt=2 06:35:52: Thread 2 - cpt=2 06:35:52: Thread 1 - cpt=2 06:35:52: Thread 1 - cpt=3 06:35:52: Thread 1 - cpt=4 06:35:52: Thread 1 - cpt=5 06:35:52: Thread 1 - cpt=6 06:35:52: Thread 1 - cpt=7 06:35:52: Thread 1 - cpt=8 06:35:52: Thread 1 - cpt=9 06:35:52: Thread 1 - cpt=10 06:35:52: Thread 2 - cpt=3 06:35:52: Thread 2 - cpt=4 06:35:52: Thread 0 - cpt=3 06:35:52: Thread 0 - cpt=4 06:35:52: Thread 0 - cpt=5 06:35:52: Thread 3 - cpt=2 06:35:52: Thread 2 - cpt=5 06:35:52: Thread 2 - cpt=6 06:35:52: Thread 3 - cpt=3 06:35:52: Thread 0 - cpt=6 06:35:52: Thread 0 - cpt=7 06:35:52: Thread 3 - cpt=4 06:35:52: Thread 3 - cpt=5 06:35:52: Thread 3 - cpt=6 06:35:52: Thread 2 - cpt=7 06:35:52: Thread 0 - cpt=8 06:35:52: Thread 0 - cpt=9 06:35:52: Thread 2 - cpt=8 06:35:52: Thread 2 - cpt=9 06:35:52: Thread 2 - cpt=10 06:35:52: Thread 2 - cpt=11 06:35:52: Thread 3 - cpt=7 06:35:52: Thread 3 - cpt=8 06:35:52: Thread 3 - cpt=9 06:35:52: Thread 3 - cpt=10 06:35:52: Thread 3 - cpt=11 06:35:52: Valeur finale = 12
Activité 2. Analyse et tests du programme ci-dessus.
- Que fait la fonction incrc ?
- Quelle doit être la valeur de COMPTEUR à la fin du programme ?
- Testez le programme plusieurs fois. La valeur est-elle toujours celle supposée ? Pourquoi ?
CORRECTION Activité 2
Pour corriger le problème identifié dans le code précédent, il faut rendre EXCLUSIF l'accès à la variable COMPTEUR. On peut pour cela utiliser un verrou. Un verrou est un objet que l'on essaye d'acquérir. Si un thread est le premier à en faire la demande, il l'acquiert. Il peut le rendre à tout moment. Si en revanche un autre thread le détient alors tous les threads qui tentent d'y accéder sont bloqués jusqu'à ce qu'il soit libéré. On construit un verrou avec la méthode Lock() du module threading. On peut alors tenter de l'acquérir avec la méthode acquire() et le rendre avec la méthode release().
NOTE : Une portion de code protégée par un verrou s'appelle une SECTION CRITIQUE. |
# Programmation concurente - Compteur partagé
# Illustration du problème de concurrence v1
import threading
import logging
import time
COMPTEUR = 0 # Ressource partagée
verrou = threading.Lock() # construction du verrou
# Fonction associée aux threads 0 à 3
def incrc(n):
global COMPTEUR
for _ in range(10):
verrou.acquire() # Acquisition du verrou
v = COMPTEUR
logging.info(f"Thread {n} - cpt={COMPTEUR}")
COMPTEUR = v + 1
verrou.release() # Relâchement du verrou
# Programme principal
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S", encoding='utf-8')
th=[] # tableau de threads
for n in range(4):
t = threading.Thread(target=incrc, args=[n])
t.start()
th.append(t)
for t in th: # Permet d'attendre que tous les threads soient terminés avant de poursuivre
t.join() # dans le programme principal
logging.info(f"Valeur finale = {COMPTEUR}") # Cette ligne est exécutée lorsque tous les threads sont terminés
06:49:00: Thread 0 - cpt=0 06:49:00: Thread 0 - cpt=1 06:49:00: Thread 0 - cpt=2 06:49:00: Thread 0 - cpt=3 06:49:00: Thread 0 - cpt=4 06:49:00: Thread 0 - cpt=5 06:49:00: Thread 0 - cpt=6 06:49:00: Thread 0 - cpt=7 06:49:00: Thread 0 - cpt=8 06:49:00: Thread 0 - cpt=9 06:49:00: Thread 1 - cpt=10 06:49:00: Thread 1 - cpt=11 06:49:00: Thread 1 - cpt=12 06:49:00: Thread 1 - cpt=13 06:49:00: Thread 1 - cpt=14 06:49:00: Thread 1 - cpt=15 06:49:00: Thread 1 - cpt=16 06:49:00: Thread 1 - cpt=17 06:49:00: Thread 1 - cpt=18 06:49:00: Thread 1 - cpt=19 06:49:00: Thread 3 - cpt=20 06:49:00: Thread 3 - cpt=21 06:49:00: Thread 3 - cpt=22 06:49:00: Thread 3 - cpt=23 06:49:00: Thread 3 - cpt=24 06:49:00: Thread 3 - cpt=25 06:49:00: Thread 3 - cpt=26 06:49:00: Thread 3 - cpt=27 06:49:00: Thread 3 - cpt=28 06:49:00: Thread 3 - cpt=29 06:49:00: Thread 2 - cpt=30 06:49:00: Thread 2 - cpt=31 06:49:00: Thread 2 - cpt=32 06:49:00: Thread 2 - cpt=33 06:49:00: Thread 2 - cpt=34 06:49:00: Thread 2 - cpt=35 06:49:00: Thread 2 - cpt=36 06:49:00: Thread 2 - cpt=37 06:49:00: Thread 2 - cpt=38 06:49:00: Thread 2 - cpt=39 06:49:00: Valeur finale = 40
Activité 3
Un verrou est créé dans le programme ci-dessus par : verrou = threading.Lock()
L'objet verrou possède deux méthodes : acquire() et release()
a) Placez le verrou dans le code ci-dessus pour protéger la section critique.
b) Testez le programme avec différentes bornes pour la boucle for. Que remarquez-vous ?
c) Expliquez pourquoi on a corrigé le problème de concurrence entre les threads t0, t1, t2 et t3.
CORRECTION Activité 3
L'interblocage se produit lorsque des processus concurrents s'attendent mutuellement. L'utilisation de plusieurs verrous rend le risque d'interblocages possible.
Dans l'exemple ci-dessous P1 et P2 sont interbloqués car :
# Illustration de linterblocage
# La fonction P1 essaye d'acquérir d'abord verrou1 puis verrou2, alors que P2 essaye de
# les acquérir dans l'ordre inverse.
# Si on exécute ce programme, il a de grandes chances de se retrouver bloqué.
import threading
import logging
verrou1 = threading.Lock()
verrou2 = threading.Lock()
def p1():
verrou1.acquire()
logging.info("P1 a acquit D1")
verrou2.acquire()
logging.info("P1 a acquit D2")
verrou2.release()
logging.info("P1 a rendu D2")
verrou1.release()
logging.info("P1 a rendu D1")
def p2():
verrou2.acquire()
logging.info("P2 a acquit D2")
verrou1.acquire()
logging.info("P2 a acquit D1")
verrou1.release()
logging.info("P2 a rendu D1")
verrou2.release()
logging.info("P2 a rendu D2")
# Programme principal
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S", encoding='utf-8')
t1 = threading.Thread(target=p1, args=[])
t2 = threading.Thread(target=p2, args=[])
t1.start()
t2.start()
Activité 4a. Analyse du programme ci-dessus
Quel pourrait être le texte affiché par le programme :
a) S'il ne se bloque pas ?
b) S'il se bloque ?
CORRECTION 4a
a) Pas d'interblocage (exemple)
b) Interblocage (1 seule solution)
Activité 4b
Supprimer l'interblocage dans le programme ci-dessous.
# Correction de linterblocage
import threading
import logging
import time
verrou1 = threading.Lock()
verrou2 = threading.Lock()
def p1():
verrou1.acquire()
logging.info("P1 a acquit D1")
verrou2.acquire()
logging.info("P1 a acquit D2")
verrou2.release()
logging.info("P1 a rendu D2")
verrou1.release()
logging.info("P1 a rendu D1")
def p2():
verrou1.acquire()
logging.info("P2 a acquit D1")
verrou2.acquire()
logging.info("P2 a acquit D2")
verrou2.release()
logging.info("P2 a rendu D2")
verrou1.release()
logging.info("P2 a rendu D1")
# Programme principal
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S", encoding='utf-8')
t1 = threading.Thread(target=p1, args=[])
t2 = threading.Thread(target=p2, args=[])
t1.start()
t2.start()
Activité 5
En vous inspirant du programme du paragraphe 2, écrivez un programme qui crée et démarre quatre fonctions concurrentes affichant plusieurs fois un message de bienvenue personnalisé (maximum dix fois le message).
Exemple de résultats attendus
Bonjour, je suis le thread 0 et ceci est le message 1
...
Message de bienvenue du thread 1 qui transmet son message 3
...
# Correction Activité 5
# Programmation concurente - Messages différents dans chaque thread
import threading
import logging
import time
'''
numth : numéro du thread
msg : tableau des messages à afficher
nb : tableau des nombres de messages
'''
# Fonction associée aux threads 0 à 3
def hello(num,msg,nb):
for i in range(nb):
logging.info(f"{msg} {i}")
logging.info(f"Thread {num}: terminé")
# Programme principal
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S", encoding='utf-8')
nb=[10,7,5,8]
msg=["Bonjour, je suis le thread 0 et ceci est le message ","Message de bienvenue du thread 1 qui transmet le message ",
"Salut, le thread 2 vous envoie le message ", "Hé, le thread 3 aussi envoie son message "]
for num in range(len(nb)):
t = threading.Thread(target=hello, args=[num,msg[num], nb[num]])
t.start()
Activité 6a
On considère un petit système embarqué : un microcontrôleur relié à trois LED A, B, C. Une LED peut être éteinte ou éclairée et on peut configurer sa couleur. On dispose de trois programmes qui affichent des signaux lumineux en faisant clignoter les LED. Chaque programme possède une LED primaire et une LED secondaire.
- Le programme P1 émet ses signaux sur A (primaire) et B (secondaire) en vert.
- Le programme P2 émet ses signaux sur B (primaire) et C (secondaire) en bleu.
- Le programme P3 émet ses signaux sur C (primaire) et A (secondaire) en rouge.
Comme les LED ne peuvent pas être configurées dans deux couleurs en même temps, le système propose deux primitives acquerirLED(nom) et rendreLED(nom) qui permettent respectivement d'acquérir et de relâcher une LED.
nom prend la valeur primaire ou secondaire.
Si une LED est déjà acquise par un programme Pi alors acquerirLED(nom) dans un programme Pj bloque Pj (i différent de j).
On suppose que chacun des trois programmes P1, P2, P3 effectue les actions suivantes en boucle :
- acquérir la LED primaire
- acquérir la LED secondaire
- configurer les couleurs
- émettre des signaux
- rendre la LED secondaire
- rendre la LED primaire puis
recommencer en 1
Montrer qu'il existe un entrelacement des exécutions qui place P1, P2 et P3 en interblocage.
CORRECTION Activié 6a
Le contexte peut être schématisé comme ci-dessous.
Activité 6b
Téléchargez, copiez et complétez le code situé ici : https://gist.github.com/WebGE/f24c17bb13f10b38eaf21725451e3754
Exemple de résultats attendus
11:21:14: LedA=vert par P1
11:21:14: LedB=vert par P1
11:21:14: LedC=rouge par P3
11:21:14: LedB relachée par P1
11:21:14: LedB=bleu par P2
11:21:14: LedA relachée par P1
11:21:14: LedA=rouge par P3
11:21:14: LedA relachée par P3
11:21:14: LedA=vert par P1
11:21:14: LedC relachée par P3
11:21:14: LedC=bleu par P2
11:21:14: LedC relachée par P2
# Correction 6b
# MICROLED - Illustration de l'interblocage dans la commande des Leds
import threading
import logging
VERROU_LED={}
VERROU_LED['A']=threading.Lock()
VERROU_LED['B']=threading.Lock()
VERROU_LED['C']=threading.Lock()
def acquerirLED(led):
VERROU_LED[led].acquire()
def rendreLED(led):
VERROU_LED[led].release()
def prog(numproc,ledprim,ledsec,couleur):
while True:
acquerirLED(ledprim)
logging.info(f"Led{ledprim}={couleur} par P{numproc}")
acquerirLED(ledsec)
logging.info(f"Led{ledsec}={couleur} par P{numproc}")
rendreLED(ledsec)
logging.info(f"Led{ledsec} relachée par P{numproc}")
rendreLED(ledprim)
logging.info(f"Led{ledprim} relachée par P{numproc}")
# Programme principal
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S", encoding='utf-8')
p1 = threading.Thread(target=prog, args=[1,'A','B','vert'])
p2 = threading.Thread(target=prog, args=[2,'B','C','bleu'])
p3 = threading.Thread(target=prog, args=[3,'C','A','rouge'])
p1.start();p2.start();p3.start();
07:09:22: LedA=vert par P1 07:09:22: LedB=vert par P1 07:09:22: LedB relachée par P1 07:09:22: LedB=bleu par P2 07:09:22: LedA relachée par P1 07:09:22: LedA=vert par P1 07:09:22: LedC=rouge par P3
Les systèmes d'exploitation multitâches sont la norme. Ils permettent d'exécuter de façon concurrente plusieurs programmes. L'exécution d'un programme s'appelle un processus. C'est le système d'exploitation et en particulier l'ordonnanceur, qui détermine quel processus s'exécute à un instant donné. Le fait pour un processus d'être interrompu s'appelle une commutation de contexte. Plusieurs processus s'exécutant de façon concurrente peuvent s'interbloquer s'ils attendent de pouvoir accéder à un même ensemble de ressources en accès exclusif. Les threads ou processus légers sont des "sous-processus" s'exécutant de manière concurrente. L'accès à des ressources par plusieurs threads peut être protégé par des verrous. Une portion de code comprise entre l'acquisition et le relâchement d'un verrou s'appelle une section critique. Numérique et Sciences Informatiques - ellipses