Dans ce pseudo-TD, on ne travaillera pas les processus mais sur les threads (thread = fil d'exécution) en Python (pseudo-TD car il s'agît plus ici d'observer et de comprendre que d'agir). Un thread est un processus simplifié. Notamment l'espace mémoire est partagé entre plusieurs thread provenant d'un même programme, ce qui n'est pas le cas entre plusieurs processus.
Sans lancer le programme ci-dessous, quelle doit être la valeur de compteur
en fin d'exécution ?
from time import sleep
compteur = 0 # Variable globale
def calcul(limite = 100000):
global compteur
for i in range(limite):
temp = compteur
# simule un traitement nécessitant des calculs
#sleep(0.000000001)
compteur = temp + 1
compteur = 0
calcul()
compteur
On va tester le code précédent exécuté simultanement par plusieurs threads. Avant exécution du code, donnez la valeur de compteur
en fin d'exécution. Vérifiez en lançant le programme, de préférence sous idle, en effet le résultat est moins intéressant sous Jupyter. Ce code est assez lent. S'il l'est trop, redémarrez le noyau et modifiez la ligne time.sleep...
afin qu'il soit plus rapide. Que constatez-vous ?
#!/usr/bin/env python
# coding: utf-8
"""
module : deadlock.py
projet : illustration des interblocages
version : 1.0
auteur : profs NSI
creation : 18/04/20
modif :
"""
import random, time
import threading
def calcul(limite):
global compteur
for i in range(limite):
tmp = compteur
# simule un traitement nécessitant des calculs
time.sleep(random.randint(1, 1000) / 10000)
compteur = tmp + 1
def workflow(nb) :
global compteur
mes_threads=[]
limite = 100
for i in range(nb): # Lance en parallèle nb fils d'exécution
mes_threads.append(threading.Thread(name=i, target=calcul, args=[limite]))
#mes_threads[i].setDaemon(True)
mes_threads[i].start()
print("thread numéro ",i," ",mes_threads[i]," vivant : ",
mes_threads[i].is_alive())
for i in range(nb):
mes_threads[i].join() # attend la fin du traitement
print("thread numéro ",i," ",mes_threads[i]," vivant : ",
mes_threads[i].is_alive())
return compteur
compteur = 0 # Variable globale
print(workflow(1))
compteur = 0 # Variable globale
print(workflow(8))
Comme on a pu le voir précédemment, le résultat attendu n'est pas retourné avec le multithreading. Pourquoi ? Il se trouve que les threads calculant "en même temps", certains calculs se "recouvrent" les uns les autres. Par exemple, si la variable compteur
vaut 5, qu'elle est appelé simultanément par les quatre threads pour l'augmenter, chacun des thread va renvoyer 6. Le résultat final des quatre incrémentation sera alors 6 et non 9.
De nombreux développeurs déconseillent leur usage : "threads are evil, don't use them". Cependant ils sont indispensables, par exemple pour gérer les connexions par milliers à un serveur.
Pour pallier au problème précédent, on introduit des verrous (lock), afin de bloquer les ressources utilisées dans la section critique, c'est-à-dire la partie du programme qui exécute les calculs sur les ressources partagées.
#!/usr/bin/env python
# coding: utf-8
"""
module : verrous.py
projet : illustration des verrous
version : 1.0
auteur : profs NSI
creation : 18/04/20
modif :
"""
import random, time
import threading
mon_verrou = threading.Lock()
def calcul(limite, verrou = None):
global compteur
for i in range(limite):
mon_verrou.acquire() # Début de la section critique
tmp = compteur
# simule un traitement nécessitant des calculs
time.sleep(random.randint(1, 100) / 10000)
if verrou is not None :
verrou.acquire()
compteur = tmp + 1
if verrou is not None :
verrou.release()
mon_verrou.release() # Fin de la section critique
def workflow(nb, limite = 100) :
global compteur
verrous_internes = [threading.Lock() for i in range(nb)]
mes_threads=[]
for i in range(nb): # Lance en parallèle nb fils d'exécution
mes_threads.append(threading.Thread(name=i, target=calcul, args=[limite]))
#p[i].setDaemon(True)
mes_threads[i].start()
print("thread numéro ",i," ",mes_threads[i]," vivant : ",
mes_threads[i].is_alive())
for i in range(nb):
mes_threads[i].join() # attend la fin du traitement
print("thread numéro ",i," ",mes_threads[i]," vivant : ",
mes_threads[i].is_alive())
return compteur
compteur = 0 # Variable globale
print(workflow(4))
Le robot Persévérance vient "d'amarsir" (sur Mars bien évidemment). Il est de connaissance commune que son objectif est de trouver des martiens et de communiquer avec eux.
Ce robot dispose de plusieurs modules indépendants, mais partageant des ressources communes. Chaque module utilise les deux ressources.
Faire tourner le programme ci-dessous. Que constatez-vous ? Expliquer.
Notes :
Images soit en partage et usage non commercial, soit de source Wikipedia Fair use, Link
# Librairie utilisées
from threading import Thread
from threading import Lock
import time
import random as rd
# Les ressources utilisées par le robot
R1_image = Lock()
R2_son = Lock()
R3_moteurs = Lock()
verrous = [(R1_image, "Image") , (R2_son, "Son"), (R3_moteurs, "Moteurs")]
def donnees_ress_th(indice, tableau):
"""
Renvoie le verrou/le thread et son nom
"""
return tableau[indice][0], tableau[indice][1]
def duTravail(thread, ressource1, ressource2):
global verrous, fils
"""...encore du travail...
Simulation de calcul
@param thread, ressource1/2 : entiers entre 0 et 2, indices des tableaux
verrous et fils. ressource1 != ressource2
"""
verrou1, nom_verrou1 = donnees_ress_th(ressource1, verrous)
verrou2, nom_verrou2 = donnees_ress_th(ressource2, verrous)
fil, nom_fil = donnees_ress_th(thread, fils)
chaine = nom_fil + " " + str(thread)+ \
" demande de la ressource " +str(ressource1)+\
" " + nom_verrou1 + "\n"
print(chaine)
verrou1.acquire()
# Commenter les lignes suivantes pour le print et constater...(décommenter le ligne
# suivante pour cela)
#"""
chaine = nom_fil + " " + str(thread)+ \
" demande de la ressource " +str(ressource2)+\
" " + nom_verrou2 + "\n"
print(chaine)
#"""
#print fonctionne ou une autre instruction, par exemple :
#time.sleep(rd.random()/100.0)
verrou2.acquire()
time.sleep(rd.random()/100.0)
verrou1.release()
verrou2.release()
chaine = nom_fil + " " + str(thread) + " fin , et libération des ressources " +\
str(ressource1) + " " + nom_verrou1 + " et " + str(ressource2) +\
" " + nom_verrou2 + "\n"
print(chaine)
def P1_communication():
"""Fonction de communication, embarquée dans le thread P1"""
while True :
print("Communication\n")
duTravail(0, 0, 1)
def P2_riposte():
"""Fonction de déplacement, embarquée dans le thread P2"""
while True :
print("Riposte !\n")
duTravail(1, 1, 2)
def P3_deplacement():
"""Fonction de deplacement, embarquée dans le thread P3"""
while True :
print("Déplacement\n" )
duTravail(2, 2, 0)
# Création des threads
t1 = Thread(target=P1_communication)
t2 = Thread(target=P2_riposte)
t3 = Thread(target=P3_deplacement)
fils = [(t1, "Communication"), (t2, "Déplacement"), (t3, "Riposte")]
# Lancement des threads
t1.start()
t2.start()
t3.start()
# Attente de la fin du travail
t1.join()
t2.join()
t3.join()
print("fin de travail\n")
Sources liste NSI : David Salle - Olivier Lécluse - et plus généralement les contributeurs de la liste
frederic.mandon@
ac-montpellier.fr, Lycée Jean Jaurès - Saint Clément de Rivière - France