Divers modules permettent de manipuler les processus à l'aide de Python :
os
qui permet, entre autres, de déterminer les caractéristiques de tel ou tel processus et de créer de nouveaux processus-filsmultiprocessing
, qui permet de lancer plusieurs "vrais" processus indépendantsLe module os
propose, entre autres, les fonctions qui font des appels systèmes au noyau du système d'exploitation pour qu'il crée de nouveaux processus.
Le module s'importe en début de script :
import os
Nous utiliserons les fonctions suivantes de ce module :
os.getpid()
: renvoie le PID du script en cours ( en fait, le PID du processus correspondant à l'interpréteur Python )os.getppid()
: renvoie le PPID du processus, c'est à dire le PID de son processus-pèreos.fork()
: crée un processus-fils à partir du processus en cours.fork()
.A cause du modèle de système d'exploitation utilisé, la fonction fork()
n'existe pas sous Windows.
infos()
qui affiche le PID et le PPID du processus en cours :
PID = 3545
PPID = 3542
nv_fils()
qui :
Je suis 6135 le fils de 6124
Je suis 6124 le père de 6135
deux_fils()
pour créer deux processus-fils du même père.fork()
?Vous allez maintenant utiliser le module multiprocessing
de Python, qui permet de lancer plus simplement des processus.
On importe les fonctions de ce module au début du script :
from multiprocessing import Process
Pour créer un processus, on l'associe à une fonction qu'il doit exécuter, en indiquant les éventuels arguments à lui passer :
p = Process(target = nom_de_la_fonction, args = [argument1, argument2, ...])
On lance alors le processus :
p.start()
Enfin, pour attendre que le processus ait terminé son exécution :
p.join()
On peut ainsi définir plusieurs processus puis les lancer successivement.
alpha()
qui affichera :
Je suis le processus 4595 et j'affiche : C
Je suis le processus 4564 et j'affiche : C
......
Pour éviter d'éventuels problèmes d'affichage dus à la trop grande rapidité d'alternance des processus, vous pouvez au besoin rajouter un petit délai après l'affichage à l'aide de la fonction sleep()
que
vous aurez importée au début du script à partir du module time
.
Par exemple : sleep(0.02)
( à adapter en fonction de la machine ).
alpha()
.Qu'en est-il dans une situation plus proche de la réalité, où un grand nombre de processus s’exécutent en "parallèle" ?
nombre()
qui affichera :
Je suis le processus 3648 et j'affiche : 5
Je suis le processus 3789 et j'affiche : 4
......
nombre()
.Dans l'exemple précédent, les processus s'exécutaient indépendamment les uns des autres, mais qu'en est-il si ils sont prévus pour traiter une même ressource ?
Normalement, les processus disposent de leur propre espace mémoire; mais il est des situations où il est nécessaire que les processus puissent échanger des données entre eux, ou alors qu'ils aient besoin de travailler sur les mêmes données.
Nous allons voir ainsi le cas de plusieurs processus destinés à modifier la valeur d'une variable commune.
Puisqu'ils disposent de leur propre espace mémoire, les processus ne peuvent pas simplement manipuler les mêmes variables; pour qu'ils puissent partager des données, il faut le faire en
utilisant des objets spéciaux appelés Value
( valeur unique ) ou Array
( tableau ).
On les importe depuis le module :
from multiprocessing import Process, Value, Array
Pour créer un objet de type Value
ou Array
:
num = Value('i', 3) # pour une valeur entière
tab = Array('d', [1.0, 2.5, 6.48]) # pour des valeurs flottantes
Ces objets peuvent être ensuite être passés comme arguments à des fonctions qui pourront les manipuler.
Notamment, pour modifier un objet de type Value
, il faut en fait modifier son attribut value
:
num.value = 3.154
incr()
qui incrémentera jusqu'à 500000 ( oui, on veut des processus qui durent un peu de temps...) un compteur compteur
qui sera partagé entre les processus.incr(compteur)
.compteur
à la fin de l'exécution des 10 processus ?Pour éviter le problème précédent, il nous faut garantir l’accès exclusif d'un seul processus à la fois à la donnée compteur
entre sa lecture et son écriture.
Pour cela on peut utiliser un verrou : c'est un objet qu’un processus peut essayer "d’acquérir"; si il est le premier à le faire ( c'est à dire si le
verrou est "libre"), il acquiert le verrou, et il a alors le "droit" d'exécuter son code.
Si un second processus essaye d’acquérir un verrou déjà pris, il sera bloqué jusqu’à ce que le verrou soit libéré.
On garantit ainsi qu'un seul processus à la fois peut accéder à une ressource donnée. La portion de code qu'il exécute alors s'appelle section critique car c'est elle qui ne doit pas être interrompue par un autre processus.
Bien entendu, à la fin de son exécution, le processus qui a pris le verrou doit alors le "libérer"...nous verrons ce qu'il se passe si ce n'est pas le cas !
En Python, un verrou ( Lock
) s'importe à partir du module multiprocessing
:
from multiprocessing import Process, Value, Lock
On le crée comme n'importe quel objet en appelant son constructeur ( sans argument ) dans le programme principal et en l'affectant à une variable.
v = Lock()
Pour qu'un processus acquiert ou libère un verrou, on utilise alors les méthodes acquire()
et release()
:
v.acquire()
......
section critique
......
v.release()
Ah oui, forcément, le temps d'exécution est beaucoup plus grand : on perd le bénéfice du "parallélisme", puisque chaque processus doit attendre qu'un autre ait fini de s'exécuter avant de pouvoir le faire...
Mais dans une situation réelle, on n'aurait pas un traitement aussi simple des données, et l'exécution de plusieurs processus "simultanés", protégée par ce système de verrou, serait quand même bénéfique au temps d'exécution...
Et donc, que se passe-t-il si un processus ne libère pas un verrou comme il est censé honnêtement le faire ? 😇
Le problème se pose quand plusieurs processus doivent accéder à plusieurs ressources communes.
Considérons la situation suivante :
A un carrefour de deux routes se présentent des véhicules qui veulent continuer tout droit; les règles du code de la route impose que c'est le véhicule venant de droite qui a la priorité.
Si les voitures n'arrivent pas au même moment au carrefour, il n'y a pas de problème; mais si elles s'y présentent toutes simultanément, aucune ne se retrouve alors plus prioritaire qu'une autre : elles se bloquent mutuellement le passage. On dit qu'il y a interblocage entre les véhicules.
De manière analogue, dans un ordinateur où s'exécutent plusieurs processus accédant aux mêmes ressources :
Par exemple, sur l'exemple ci-contre, quand le processus 1 arrive au carrefour :
De même, quand le processus 3 arrive au carrefour :
Là aussi, tant que les processus s’exécutent sagement les uns après les autres, il n'y a pas de problème...mais on sait que ce n'est pas le cas !
L'ordonnancement non prévisible des processus par le système d'exploitation fait que le processus 2 peut très bien déjà avoir acquis le verrou sur le passage 2, et bloque
donc également le passage 3, qui lui-même bloque peut-être le passage de 4, qui lui-même bloque 1...
Selon le moment où chaque processus s'exécute, on arrive donc aussi à une situation d'interblocage, chaque processus attendant une ressource qui ne peut être libérée que par un autre processus...
Le script ci-dessous illustre cette situation :
from multiprocessing import Process, Lock
from random import random
from time import sleep
v1 = Lock()
v2 = Lock()
v3 = Lock()
v4 = Lock()
def passer1():
while True:
v2.acquire()
v1.acquire()
print('Passage voiture Route 1')
sleep(0.02)
v1.release()
v2.release()
def passer2():
while True:
v3.acquire()
v2.acquire()
print('Passage voiture Route 2')
sleep(0.02)
v2.release()
v3.release()
def passer3():
while True:
v4.acquire()
v3.acquire()
print('Passage voiture Route 3')
sleep(0.02)
v3.release()
v4.release()
def passer4():
while True:
v1.acquire()
v4.acquire()
print('Passage voiture Route 4')
sleep(0.02)
v4.release()
v1.release()
p1 = Process(target = passer1, args = [])
p2 = Process(target = passer2, args = [])
p3 = Process(target = passer3, args = [])
p4 = Process(target = passer4, args = [])
p1.start()
p2.start()
p3.start()
p4.start()
p1.join()
p2.join()
p3.join()
p4.join()
Une situation d'interblocage se produit donc quand chaque processus attend une ressource qui ne peut être libérée que par un autre processus.
Les problèmes d'interblocage peuvent se produire de manière imprévisible dans un système, et sont toujours très délicats à résoudre...
Avec une ligne de commande dans un terminal sous Linux, on obtient l'affichage suivant :
La documentation Linux donne la signification de quelques uns des champs :