Bonjour à tous ! Depuis quelques temps je m'intéresse à un principe tout simple, faire communiquer plusieurs tout petits scripts entre eux pour faire le travail d'un programme plus grand : c'est ce qu'on appelle de l'algorithmique distribuée, très utile à l'heure où quasiment tout le monde utilise des ordinateurs multi-coeurs qui sont spécialisés pour cela. Mais en faisant ce genre d'architecture, on se heurte à des soucis de partage mémoire et d'accès concurrents, les scripts se marchant sur les pieds les uns les autres... D'où une "police" de non-interférence : la sémaphorisation !
Introduction :
Pour ceux qui ne savent pas ce qu'est la sémaphorisation, il faut d'abord se demander comment marche un ordinateur pour faire fonctionner deux programmes à la fois : en effet, il est tout à fait possible de lancer un Bloc-Notes et la Calculatrice en même temps, une vidéo et Firefox en parallèle, etc... pourtant quand on travaille sur un, les autres ne s'arrêtent pas de tourner pour autant. Cela est dû à ce qu'on appelle un tourniquet (cf tourniquet/noyau/gestion de processus) : Chaque programme a un processus, chaque processus est identifié par un PID (processus ID), et le tourniquet donne la parole à chaque processus à la suite en changeant toutes les millisecondes/microsecondes/nanosecondes par exemple; ainsi les processus semblent tourner ensemble en même temps, mais en réalité, ils effectuent un bout de chemin chacun leur tour.
Cela permet à l'ordinateur de faire plusieurs choses à la fois de notre point de vue alors que de base le processeur de l'ordinateur ne fait qu'une seule opération à la fois. Mais que se passe-t'il lorsque deux programmes tentent de faire la même chose au même moment? Pour voir ce qui peut poser problème, créez un fichier texte toto.txt, ouvrez le dans deux Bloc-Notes différents, écrivez quelque chose dans chaque fenêtre et sauvegardez : Seule la dernière sauvegarde sera prise en compte, la première sera perdue.
Catastrophe !
Cela peut être très problématique quand plusieurs scripts AutoIt sont en cours simultanément.
Voici un exemple : Je lance 10 scripts en même temps qui n'ont qu'une seule tache : lire une variable dans la base de registre et l'incrémenter 1000 fois. On s'attend donc que lorsque tous les processus ont fini leur travail, la clef dans la base de registre ait augmenté de 10 x 1000 = 10.000; logique, non?
Pour être sûr qu'ils se lancent en même temps, je lance tout d'abord un script de type "Père" qui lance lui même 10 scripts "Fils", qui attendent le signal du père pour faire leur travail :
► Afficher le texte10 processus en même temps
Code : Tout sélectionner
#RequireAdmin
; Préparation
Global Const $LAUNCH_PARAMETER = "JE_SUIS_UN_FILS"
Global $NOM, $DATA[10]
If StringLower($cmdLine[$cmdLine[0]]) <> $LAUNCH_PARAMETER Then
; Je suis un père, je lance 10 fois mes fils
$NOM = "PERE"
For $i = 0 To 9
$DATA[$i] = Run(@AutoItExe&' "'&@ScriptFullPath&'" '&($i+1)&' '&$LAUNCH_PARAMETER, @ScriptDir)
Next
Else
; Je suis un fils, j'attends le top départ de mon père pour incrémenter une variable
$NOM = "FILS"
$DATA[0] = Number($cmdLine[1])
$DATA[1] = @AutoItPID
EndIf
; Corps du script
Global $TITRE = "PERE : TOP DEPART !", $X = $DATA[0]*@DesktopWidth/11
Switch $NOM
Case "PERE" ; Corps du script du père
; J'initialise la clef à zéro
RegWrite("HKEY_CURRENT_USER\Software\Test", "Increment", "REG_SZ", 0)
Sleep(1000)
; J'attends que tous mes fils soient là
Do
$exist = True
For $i = 0 To 9
$exist = $exist And ProcessExists($DATA[$i])
Next
Until $exist = True
; A vos marques... (3s)
Sleep(3000)
; Je lance le top départ ! (j'affiche une fenêtre que tous mes fils verront
Local $GUI = GUICreate($TITRE,300,50)
GUISetState(@SW_SHOW, $GUI)
Sleep(2000)
GUIDelete($GUI)
; J'attends que tous mes fils aient fini leur travail
Do
$exist = False
For $i = 0 To 9
$exist = $exist Or ProcessExists($DATA[$i])
Next
Until $exist = False
; Je regarde le résultat obtenu
Local $resultat = Number(RegRead("HKEY_CURRENT_USER\Software\Test", "Increment"))
MsgBox(0,"PERE","Valeur en base : '"&$resultat&"'"&@CRLF&"Il y a donc eu "&(10*1000-$resultat)&" pertes.")
Case "FILS" ; Corps du script du fils
; J'affiche un message disant que je suis prêt
ToolTip("Fils N°"&$DATA[0]&" (PID="&$DATA[1]&") en attente",$X, 5)
; J'attends le top départ de mon père
While WinActive($TITRE) = 0
Sleep(100)
WEnd
; Le top départ est lancé !
For $i = 1 To 1000
ToolTip("Fils N°"&$DATA[0]&" : "&$i&"/1000",$X, 5)
; Je lis la valeur dans "Increment", le lui ajoute 1, et je l'écris dans "Increment"
RegWrite("HKEY_CURRENT_USER\Software\Test", "Increment", "REG_SZ", Number(RegRead("HKEY_CURRENT_USER\Software\Test", "Increment"))+1)
Next
; Ça y est, j'ai fini, je m'éteins.
EndSwitch
Et... Un message s'affiche disant que je n'ai pas 10.000 dans la variable, mais aux alentours de 8.000, ce qui veut dire que j'ai 20% des opérations qui se sont vautrées ! Comment cela se fait-il? Pourtant le code est bon...!
Cela est dû justement au tourniquet, qui met en pause un "fils" pour donner la main à un autre, c'est un peu aléatoire, c'est pour cela que tous les processus ne vont pas à la même vitesse. Et si cela se produit au mauvais moment, il peut se produire "un accès concurrent" à la variable. Exemple de situation :
► Afficher le texteSituation entre trois processeurs
[...]
- Je suis le processus N°1
- Je suis à mon 20ème tour de boucle
- Je lis la variable, c'est écrit 150
HOP! Je suis le tourniquet, t'as assez travaillé, donne la parole à ton frère ! N°1, endors-toi; N°2, prend la main.
- Je suis le processus N°2
- Je suis à mon 15ème tour de boucle (j'ai du retard, tiens)
- Je lis la variable, c'est écrit 150
- Je lui ajoute 1, ça donne 151
- J'écris 151 dans la variable
RE-HOP! Je suis le tourniquet, et je donne maintenant la main à n°3 ! N°2, rendors-toi; N°3, à ton tour.
- Je suis le processus N°3
- Je suis à mon 17ème tour de boucle (au moins je suis pas dernier)
- Je lis la variable, c'est écrit 151
- Je lui ajoute 1, ça donne 152
- J'écris 152 dans la variable
- Je fais un 18ème tour de boucle tant que j'y suis
- Je lis la variable, c'est écrit 152
- Je lui ajoute 1, ça donne 153
- J'écris 153 dans la variable
RE-RE-HOP! Je suis le tourniquet, et je redonne la main à n°1 ! N°3, rendors-toi; N°1, reprends là où tu en étais.
- Je suis le processus n°1
- J'en suis où? ah oui, j'ai lu la variable, c'était écrit 150
- J'ajoute 1, ça me donne 151
- J'écris 151 dans la variable (sans savoir que N°2 et N°3 avaient déjà écrit quelque chose, les informations sont perdues)
[...]
Comment faire pour que cette situation ne se produise pas? Il faudrait pouvoir dire qu'il ne faut pas interrompre un processus entre la lecture d'une variable et l'écriture dans celle-ci. C'est ce qu'on appelle le principe d'atomicité (le fait qu'une opération ne doit pas être divisible). Il existe des garde-fous pour "encadrer" des lignes de codes, et dire aux autres processus qu'il faut attendre : Il s'agit des sémaphores.
Les sémaphores
Un sémaphore est un objet du noyau du système d'exploitation (ou kernel) qui met en attente un processus lorsque celui ci fait appel à lui et que certaines conditions ne sont pas remplies. Les sémaphores n'ont que trois opérations de base:
- Init : création du sémaphore avec un nombre de jetons.
- P : prise d'accès au sémaphore (
Puis-je?)
- V : relâchement du sémaphore (
Vas-y!)
Les sémaphores les plus simples à comprendre sont aussi ceux qui nous intéressent ici : les sémaphores Mutex (Exclusion mutuelle). Les mutex ont un nombre de jetons égal à 1 (cf Complément pour plus d'informations).
Le mutex est un sémaphore qui ne laisse la main qu'au premier qui demande avec la commande P (il passe en blocage), les suivants qui font un P sont mis en attente dans une file, et quand le premier le débloque (avec la commande V), il donne la main à celui qui est à la suite dans sa file d'attente, et ainsi de suite... Les processus sont des orateurs qui lèvent la main (P), la baisse quand ils ont fini leur discours (V), le mutex devient alors le micro pour parler.
Ainsi, l'exclusion mutuelle se met en place, empêchant deux processus d'accéder en même temps à une même variable. Reprenons l'exemple précédent des trois processus qui se disputent :
► Afficher le texteSituation entre trois processeurs AVEC mutex
(Un mutex "MonMutex" est créé)
[...]
- Je suis le processus N°1
- Je suis à mon 20ème tour de boucle
- Je fais un P sur "MonMutex", il me donne l'accès donc je continue
- Je lis la variable, c'est écrit 150
HOP! Je suis le tourniquet, t'as assez travaillé, donne la parole à ton frère ! N°1, endors-toi; N°2, prend la main.
- Je suis le processus N°2
- Je suis à mon 15ème tour de boucle (j'ai du retard, tiens)
- Je fais un P sur "MonMutex", il me refuse l'accès donc je me met en attente
RE-HOP! Je suis le tourniquet, et je donne maintenant la main à n°3 puisque n°2 pionce comme un loir! N°3, à ton tour.
- Je suis le processus N°3
- Je suis à mon 17ème tour de boucle (au moins je suis pas dernier)
- Je fais un P sur "MonMutex", il me refuse l'accès donc je me met en attente
RE-RE-HOP! Je suis le tourniquet, et je redonne la main à n°1 puisque n°3 fait dodo! N°1, reprends là où tu en étais.
- Je suis le processus n°1
- J'en suis où? ah oui, "MonMutex" m'a donné l'accès
- Je lis la variable, c'est écrit 150
- J'ajoute 1, ça me donne 151
- J'écris 151 dans la variable
- Je fais un V sur "MonMutex" pour le libérer
TIC! Je suis le tourniquet, et je donne la main à n°3, je suis d'humeur taquine! N°1, en stand-by; N°3, reprends là où tu en étais.
- Je suis le processus N°3
- Je suis en attente, le sémaphore ne m'a pas donné l'accès
- Zzzzzzz... (et non je ne ronfle pas!)
TAC! Je suis le tourniquet, et puisque n°3 ne peut/veut rien faire je donne la main à n°2! N°2, à toi.
- Je suis le processus N°2
- "MonMutex" m'a donné la main, je continue mon travail
- Je lis la variable, c'est écrit 151
- J'ajoute 1, ça fait 152
- J'écris 152 dans la variable
- Je fais un V sur "MonMutex" pour le libérer
TOC! Toujours le tourniquet, et je file le jeton à n°3.
- Je suis le processus N°3
- "MonMutex" m'a donné la main (enfin!), je continue mon travail
- Je lis la variable, c'est écrit 152
- J'ajoute 1, ça fait 153
- J'écris 153 dans la variable
- Je fais un V sur "MonMutex" pour le libérer
- Dans la foulée je fais un 18ème tour
- Je fais un P sur "MonMutex", puisqu'il est libre (je viens de le libérer) je prends la main
- Je lis la variable, c'est écrit 153
- J'ajoute 1, ça fait 154
- J'écris 154 dans la variable
- Je fais un V sur "MonMutex" pour le libérer
(aucune information n'a été perdue)
[...]
Voici une situation où le mutex permet aux processus de ne pas écraser le travail des autres processus. En adaptant ce principe d'atomicité à notre script des 10 processus, on voit qu'il n'y a que de très légères modifications à effectuer :
- Créer un mutex => Func _Semaphore_MutexInit($nom) : $handle
- Avant de lire la clef de registre, demander l'accès au mutex => Func _Semaphore_P($handle) : void
- Après avoir écrit dans la clef de registre, redonner l'accès au mutex => Func _Semaphore_V($handle) : void
Voici ce que cela donne (le fichier Semaphore.au3 est en pièce jointe de ce message):
► Afficher le texte10 processus en même temps AVEC MUTEX
Code : Tout sélectionner
#RequireAdmin
#Include "Semaphore.au3"
; Préparation
Global Const $LAUNCH_PARAMETER = "JE_SUIS_UN_FILS"
Global $NOM, $DATA[10]
If StringLower($cmdLine[$cmdLine[0]]) <> $LAUNCH_PARAMETER Then
; Je suis un père, je lance 10 fois mes fils
$NOM = "PERE"
For $i = 0 To 9
$DATA[$i] = Run(@AutoItExe&' "'&@ScriptFullPath&'" '&($i+1)&' '&$LAUNCH_PARAMETER, @ScriptDir)
Next
Else
; Je suis un fils, j'attends le top départ de mon père pour incrémenter une variable
$NOM = "FILS"
$DATA[0] = Number($cmdLine[1])
$DATA[1] = @AutoItPID
EndIf
; Corps du script
Global $TITRE = "PERE : TOP DEPART !", $X = $DATA[0]*@DesktopWidth/11
Switch $NOM
Case "PERE" ; Corps du script du père
; J'initialise la clef à zéro
RegWrite("HKEY_CURRENT_USER\Software\Test", "Increment", "REG_SZ", 0)
Sleep(1000)
; J'attends que tous mes fils soient là
Do
$exist = True
For $i = 0 To 9
$exist = $exist And ProcessExists($DATA[$i])
Next
Until $exist = True
; A vos marques... (3s)
Sleep(3000)
; Je lance le top départ ! (j'affiche une fenêtre que tous mes fils verront)
Local $GUI = GUICreate($TITRE,300,50)
GUISetState(@SW_SHOW, $GUI)
Sleep(2000)
GUIDelete($GUI)
; J'attends que tous mes fils aient fini leur travail
Do
$exist = False
For $i = 0 To 9
$exist = $exist Or ProcessExists($DATA[$i])
Next
Until $exist = False
; Je regarde le résultat obtenu
Local $resultat = Number(RegRead("HKEY_CURRENT_USER\Software\Test", "Increment"))
MsgBox(0,"PERE","Valeur en base : '"&$resultat&"'"&@CRLF&"Il y a donc eu "&(10*1000-$resultat)&" pertes.")
Case "FILS" ; Corps du script du fils
; Je récupère un lien vers le mutex
Local $mutex = _Semaphore_MutexInit("MonMutex")
; J'affiche un message disant que je suis prêt
ToolTip("Fils N°"&$DATA[0]&" (PID="&$DATA[1]&") en attente",$X, 5)
; J'attends le top départ de mon père
While WinActive($TITRE) = 0
Sleep(100)
WEnd
; Le top départ est lancé !
For $i = 1 To 1000
ToolTip("Fils N°"&$DATA[0]&" : "&$i&"/1000",$X, 5)
; Je demande l'accès au mutex
_Semaphore_P($mutex)
; Je lis la valeur dans "Increment", le lui ajoute 1, et je l'écris dans "Increment"
RegWrite("HKEY_CURRENT_USER\Software\Test", "Increment", "REG_SZ", Number(RegRead("HKEY_CURRENT_USER\Software\Test", "Increment"))+1)
; Je rends l'accès au mutex
_Semaphore_V($mutex)
Next
; Ca y est, j'ai fini, je m'éteins.
EndSwitch
Et... Voila! 10 scripts comptant chacun jusque 1.000, et le résultat obtenu est bien de 10.000 !
Conclusion
Je me doute bien que ce souci n'arrive pas à tout le monde, mais ce genre de problème de partage se produit plus souvent qu'on ne le pense. J'espère ne pas en avoir trop fait à vouloir vulgariser ce phénomène ou ses implications/applications, et que vous aurez retenu quelque chose de ce pavé
Les sémaphores sont utiles pour faire fonctionner plusieurs scripts simultanément sur les même ressources, mais une mauvaise programmation peut entrainer des blocages. De plus, il n'existe pas que les mutex, et rien (au contraire !) n'empêche d'utiliser plusieurs sémaphores dans un même script/dans un même ensemble de scripts
(rendez-vous de processus, pilotage et mise en pause, etc...) Mais cela fera partie d'un complément de ce tuto
Merci à tous, et à bientôt !
PJ: Mini-UDF de gestion des sémaphores (code source également disponible ci dessous) :
► Afficher le texteSemaphore.au3
Code : Tout sélectionner
#include-once
; #INDEX# =======================================================================================================================
; Title .........: Semaphore UDF
; AutoIt Version : 3.3.6.1
; Description ...: Kernel libraries calls that have been translated to AutoIt functions, in order to manage semaphores and mutex.
; Author(s) .....: Aurélien A. (ZDS)
; Dll ...........: kernel32.dll
; ===============================================================================================================================
; #CURRENT# =====================================================================================================================
; _Semaphore_Init
; _Semaphore_MutexInit
; _Semaphore_BlockerInit
; _Semaphore_P
; _Semaphore_V
; ===============================================================================================================================
; #FUNCTION# ====================================================================================================================
; Name...........: _Semaphore_Init
; Description ...: Create a defined semaphore and return the SemID (or get the SemID of an existing semaphore)
; Syntax.........: _Semaphore_Init($nom, $valeur, $valeurMax)
; Parameters ....: $nom - Name of the semaphore.
; $valeur - Value to set in the semaphore counter.
; + $valeur must be is greater than or equal to 0.
; $valeurMax - Value to set as the maximum value for the semaphore counter.
; + $valeurMax must be is greater than or equal to $valeur, and greater than 0.
; Return values .: Success - Handle to the semaphore
; Failure - 0 and @error is set
; Author ........: Aurélien A. (ZDS)
; Modified.......:
; Remarks .......:
; Related .......:
; Link ..........: @@MsdnLink@@ CreateSemaphoreW
; Example .......:
; ===============================================================================================================================
Func _Semaphore_Init($nom, $valeur, $valeurMax)
If $valeur>$valeurMax Then SetError(1, 0, 0)
Local $resultat = DllCall("kernel32.dll", "ptr", "CreateSemaphoreW", _
"ptr", DllStructGetPtr(0), _
"int", $valeur, _
"int", $valeurMax, _
"wstr", $nom _
)
If @error Then Return SetError(1, 0, 0)
$resultat = $resultat[0]
If Not $resultat Then Return SetError(1, 0, 0)
Return $resultat
EndFunc ;==>_Semaphore_Init
; #FUNCTION# ====================================================================================================================
; Name...........: _Semaphore_MutexInit
; Description ...: Create a defined mutex and return the SemID (or get the SemID of an existing mutex)
; Syntax.........: _Semaphore_MutexInit($nom)
; Parameters ....: $nom - Name of the mutex
; Return values .: Success - Handle to the mutex
; Failure - 0 and @error is set
; Author ........: Aurélien A. (ZDS)
; Modified.......:
; Remarks .......:
; Related .......:
; Link ..........: @@MsdnLink@@ CreateSemaphoreW
; Example .......:
; ===============================================================================================================================
Func _Semaphore_MutexInit($nom)
Local $resultat = _Semaphore_Init($nom,1,1)
If @error Then Return SetError(1, 0, 0)
Return $resultat
EndFunc ;==>_Semaphore_MutexInit
; #FUNCTION# ====================================================================================================================
; Name...........: _Semaphore_BlockerInit
; Description ...: Create a defined blocking mutex and return the SemID (or get the SemID of an existing blocking mutex)
; Syntax.........: _Semaphore_BlockerInit($nom)
; Parameters ....: $nom - Name of the blocking mutex
; Return values .: Success - Handle to the blocking mutex
; Failure - 0 and @error is set
; Author ........: Aurélien A. (ZDS)
; Modified.......:
; Remarks .......:
; Related .......:
; Link ..........: @@MsdnLink@@ CreateSemaphoreW
; Example .......:
; ===============================================================================================================================
Func _Semaphore_BlockerInit($nom)
Local $resultat = _Semaphore_Init($nom,0,1)
If @error Then Return SetError(1, 0, 0)
Return $resultat
EndFunc ;==>_Semaphore_BlockerInit
; #FUNCTION# ====================================================================================================================
; Name...........: _Semaphore_P
; Description ...: Call a semaphore with a P operation ("take"/"wait")
; Syntax.........: _Semaphore_P($semaphore)
; Parameters ....: $semaphore - Handle to the semaphore
; Return values .: Success - Not 0
; Failure - 0 and @error is set
; Author ........: Aurélien A. (ZDS)
; Modified.......:
; Remarks .......:
; Related .......:
; Link ..........: @@MsdnLink@@ WaitForSingleObject
; Example .......:
; ===============================================================================================================================
Func _Semaphore_P($semaphore)
Local $resultat = DllCall("kernel32.dll", "int", "WaitForSingleObject", _
"handle", $semaphore, _
"dword", -1 _
)
If @error Then Return SetError(1, 0, 0)
$resultat = $resultat[0]
Return $resultat
EndFunc ;==>_Semaphore_P
; #FUNCTION# ====================================================================================================================
; Name...........: _Semaphore_V
; Description ...: Call a semaphore with a V operation ("release"/"signal")
; Syntax.........: _Semaphore_V($semaphore)
; Parameters ....: $semaphore - Handle to the semaphore
; Return values .: Success - Not 0
; Failure - 0 and @error is set
; Author ........: Aurélien A. (ZDS)
; Modified.......:
; Remarks .......:
; Related .......:
; Link ..........: @@MsdnLink@@ ReleaseSemaphore
; Example .......:
; ===============================================================================================================================
Func _Semaphore_V($semaphore)
Local $resultat = DllCall("kernel32.dll", "int", "ReleaseSemaphore", _
"ptr", $semaphore, _
"int", 1, _
"int*", 0 _
)
If @error Then Return SetError(1, 0, 0)
$resultat = $resultat[0]
If Not $resultat Then Return SetError(1, 0, 0)
Return $resultat
EndFunc ;==>_Semaphore_V
NB: La fonction SemInit() permet de créer un sémaphore, où de récupérer son $handle si celui ci existe déjà.
PS: Pour plus d'informations sur les sémaphores :
http://fr.wikipedia.org/wiki/Sémaphore_(informatique)
EDIT 23/05/2011 : Mise à jour du code pour correspondre aux UDFs avec documentation AutoIt.