Connexion élèves

Choisir le(s) module(s) à installer :

Python et les processus

Divers modules permettent de manipuler les processus à l'aide de Python :

Création de processus-fils

Le 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ère
  • os.fork() : crée un processus-fils à partir du processus en cours.
    Comme cette fonction crée une copie du processus appelant, chacun des deux processus exécutera alors le même code qui suit l'appel de fork().
    La fonction n'est appelée qu'une seule fois, mais renvoie une valeur différente dans chaque copie :
    • 0 dans le processus-fils
    • le PID du processus-fils dans le processus-père
    • une valeur négative si la création du processus-fils a échoué
    Cette valeur permet donc de savoir dans quel processus ( père ou fils ) le script "se trouve".

A cause du modèle de système d'exploitation utilisé, la fonction fork() n'existe pas sous Windows.

Fork Python
  1. écrire une fonction infos() qui affiche le PID et le PPID du processus en cours :
    
    PID = 3545
    PPID = 3542						
    					
  2. écrire une fonction nv_fils() qui :
    • crée un processus-fils à partir du processus principal
    • affiche le PID et le PPID du processus en cours d'exécution en distinguant les processus père et fils :
      
      Je suis 6135 le fils de 6124
      Je suis 6124 le père de 6135	
      							
  3. Lancer plusieurs fois de suite le script précédent. Que constate-t-on ? Comment expliquer cela ?
  4. pour encore mieux visualiser ceci, écrire une fonction deux_fils() pour créer deux processus-fils du même père.
    Attention, ce n'est pas évident : où dans le script doit-on placer le deuxième appel à la fonction fork() ?

SOLUTION

Les difficultés de la gestion des processus

Lancement de processus

Vous allez maintenant utiliser le module multiprocessingde 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.

Deux processus concurrents

  1. écrire une fonction alpha() qui affichera :
    • le PID du processus qui l’exécute, valeur passée en argument à la fonction
    • les lettres de l'alphabet dans l'ordre
    
    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 ).

  2. créer 2 processus qui exécuteront en concurrence la fonction alpha().
    Attention à les démarrer tous les deux successivement avant d'attendre la fin de leur exécution.
  3. Observer et interpréter l'affichage obtenu.

SOLUTION

Encore plus de processus

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" ?

  1. écrire une fonction nombre() qui affichera :
    • le PID du processus qui l’exécute, valeur passée en argument à la fonction
    • une valeur entière croissante de 1 à 10
    
    Je suis le processus 3648 et j'affiche : 5	
    Je suis le processus 3789 et j'affiche : 4
    ......			
    					
  2. créer 10 processus qui exécuteront en concurrence la fonction nombre().
    On aura intérêt à :
    • à créer et lancer les processus dans une boucle, le compteur de boucle indiquant le numéro du processus lancé.
    • stocker les processus dans une liste pour un accès plus facile
    • utiliser une autre boucle pour attendre la fin de l'exécution de chaque processus ( si l'on faisait cela dans la même boucle que celle de création des processus, on devrait attendre la fin de l’exécution de chacun avant de lancer le suivant : aucun intérêt ! ).
  3. lancer le script plusieurs fois de suite; interpréter l'affichage observé.

SOLUTION

Le problème de l'accès à des ressources communes

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	
			
  1. écrire une fonction 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.
  2. créer 10 processus qui exécuteront en concurrence la fonction incr(compteur).
  3. Quelle valeur aura compteur à la fin de l'exécution des 10 processus ?
  4. lancer le script plusieurs fois de suite; interpréter l'affichage observé.

SOLUTION

La solution : les verrous

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()
			
  1. dans le script précédent, identifier la section critique de la fonction exécutée par les processus.
  2. utiliser alors le mécanisme de verrou décrit ci-dessus pour "protéger" cette section critique.
  3. lancer plusieurs fois de suite le script pour constater la disparition du problème précédent.

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...

SOLUTION

Le problème de l'interblocage

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 :

  • chaque processus correspond à une voiture; la ressource utilisée par le processus est le passage tout droit
  • un processus se présente au carrefour : le processus acquiert alors un verrou sur son passage tout droit.
  • cependant, il a aussi priorité sur le processus venant à sa gauche : il acquiert donc également un verrou qui "bloque" le processus venant à sa gauche.

Par exemple, sur l'exemple ci-contre, quand le processus 1 arrive au carrefour :

  • il acquiert un verrou sur le passage 1
  • il acquiert également un verrou sur le passage 2

De même, quand le processus 3 arrive au carrefour :

  • il acquiert un verrou sur le passage 3
  • il acquiert également un verrou sur le passage 4
Interblocage à un 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()
			
  1. lancer plusieurs fois de suite ce script. Que constate-t-on ?
  2. pour "tuer" le script, utiliser l'icône au dessus de l'interpréteur dans Pyzo ( ou alors utiliser les commandes du Terminal vues au chapitre précédent ! )

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...

QCM d'entraînement ( d'après Bac )

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 :