241 lines
8.5 KiB
Markdown
241 lines
8.5 KiB
Markdown
<header><h1>
|
||
Programmation concurrente
|
||
</h1></header>
|
||
|
||
Un programme concurrent est un programme non séquentiel.
|
||
Soit l'expression arithmétique suivante : $(a+b) * (c+d)$.
|
||
|
||
***Programmation séquentiel***
|
||
1. $x = (a+b)$
|
||
2. $y = (c+d)$
|
||
3. $x*y$
|
||
|
||
***Programmation concurrente***
|
||
1. $x = (a+b) \qquad y = (c+d)$
|
||
2. $x*y$
|
||
|
||
La programmation concurrente sur 1 CPU permet d'exploiter pleinement les périphériques parallèlement au processeur.
|
||
Cela permet d'éviter de bloquer toutes les applications lorsqu'une seule est en attente d'une réponse.
|
||
|
||
La programmation concurrente sur plusieurs CPU permet en plus un gain de temps.
|
||
|
||
# Exclusion mutuelle
|
||
un programme est une entité statique
|
||
un processus est une entité dynamique
|
||
|
||
Les processus ne sont jamais totalement indépendants.
|
||
Ils partagent des ressources (mémoire, fichiers, périphériques, variable, etc.)
|
||
|
||
Ceci peut conduire à des erreurs et doit donc être controlé.
|
||
|
||
- ***Section critique :*** partie du code qui accède à une ressource partagée.
|
||
- ***Exclusion mutuelle :*** un seul processus à la fois peut accéder à la section critique.
|
||
- ***Ressource critique :*** ressource qui doit être accédé en exclusion mutuelle
|
||
|
||
On pourrait éviter les problèmes d'exclusion mutuelle si on pouvait assurer que deux processus n'exécute jamais en même temps une section
|
||
critique.
|
||
On peut résoudre ce problème à l'aide de **mutex** ou **sémaphore**.
|
||
|
||
## Exclusion mutuelle par attente active
|
||
|
||
```pascal
|
||
type processNum = 1..2;
|
||
|
||
module CS; /* Section critique */
|
||
defines Enter, Leave; /* Primitive */
|
||
|
||
var
|
||
request : array processNum of boolean;
|
||
tour : processNum;
|
||
procedure Enter(num: processNum);
|
||
begin
|
||
request[num] := true; /* Demande d'accès */
|
||
tour := 3 – num; /* Autre processus */
|
||
repeat
|
||
/* Attendre que l'autre processus ait fini avec "not request" */
|
||
/* Priorité si les deux processus demandent en même temps à la section critique
|
||
grâce à "tour = num" */
|
||
until (not request[3-num]) or (tour = num)
|
||
end Enter;
|
||
|
||
procedure Leave(num: processNum);
|
||
begin
|
||
request[num] := false
|
||
end Leave;
|
||
|
||
begin /* Initialisation */
|
||
request[1] := false;
|
||
request[2] := false
|
||
end CS;
|
||
|
||
/***********************************************************************/
|
||
|
||
process P1;
|
||
uses CS, Console;
|
||
begin
|
||
CS.Enter(1);
|
||
Console.Write(”Error on hard disk !”);
|
||
CS.Leave(1);
|
||
end P1;
|
||
|
||
/***********************************************************************/
|
||
|
||
process P2;
|
||
uses CS, Console;
|
||
begin
|
||
CS.Enter(2);
|
||
Console.Write(”Printer not ready !”);
|
||
CS.Leave(2);
|
||
end P2;
|
||
```
|
||
|
||
- si les processus *P1* et *P2* ne tentent pas de pénétrer simultanément dans la section
|
||
critique, la variable *request[3-num]* est fausse et l'entrée en section critique est
|
||
autorisée
|
||
- si par contre les processus *P1* et *P2* tentent de pénétrer simultanément dans la section
|
||
critique, la variable *tour* permet d'arbitrer entre les deux demandes. La variable
|
||
*tour* (à la ligne 15) indique le numéro du processus élu
|
||
|
||
Cette méthode peut facilement être généralisée à n processus. Il suffit de faire *tour := n+1 – num* et
|
||
*request[n+1-num]*.
|
||
|
||
Cette solution n'est pas satisfaisante, car elle consomme beaucoup de ressources CPU. En effet, un processus
|
||
qui attend l'accès à la section critique est en attente active. Il est donc en boucle constante et consomme
|
||
beaucoup de ressources CPU.
|
||
|
||
|
||
## Exclusion mutuelle par masquage des interruptions
|
||
Masquer les interruptions pour résoudre le problème de l'exclusion mutuelle.
|
||
Plus d'interruption $=>$ plus de commutation de processus $=>$ plus de risque de violer l'exclusion mutuelle.
|
||
|
||
Le danger avec cette solution est le risque de perte d'information provenant des périphériques. Cela ne peut être une bonne solution
|
||
que si la section critique est très courte. Il est dangereux de laisser la possibilité de masquer les interruptions à l'utilisateur.
|
||
|
||
|
||
## Instruction TAS (TestAndSet)
|
||
L'exclusion mutuelle par masquage des interruptions n'est pas possible dans une architecture multiprocesseur.
|
||
Il est possible de résoudre le problème de l'exclusion mutuelle en utilisant l'instruction *TAS*.
|
||
Elle permet de tester et de modifier une variable en **une seule instruction** assembleur.
|
||
|
||
|
||
# Mutex
|
||
Le mutex est utilisé pour bloquer l'accès simultané aux sections critiques avec les primitives suivantes :
|
||
- ***block() :*** Il laisse passer un seul processus à la fois. Si la section est déjà bloquée, le processus et mis dans
|
||
une file d'attente.
|
||
```pascal
|
||
procedure Block(var v:lock);
|
||
begin
|
||
if v.state = OPEN then
|
||
v.state := CLOSE
|
||
else
|
||
block the process in the list "v.waiting"
|
||
end if
|
||
end Block;
|
||
}
|
||
```
|
||
- ***release() :*** Il libère la section critique, si la file d'attente est vide. Sinon, il passe la main au processus
|
||
suivant.
|
||
```pascal
|
||
procedure Release(var v:lock);
|
||
begin
|
||
if queue "v.waiting" not empty then
|
||
unblock first process in the list "v.waiting"
|
||
else
|
||
v.state := OPEN
|
||
end if
|
||
end Release;
|
||
}
|
||
```
|
||
Ces procédures sont elles même des sections critiques. Elles sont cepandant très courte et ne contiennent aucunes entrées-sorties.
|
||
Elles peuvent donc être implémentées à l'aide d'une [Exclusion mutuelle par masquage des interruptions](#exclusion-mutuelle-par-masquage-des-interruptions)
|
||
ou d'une [instruction TAS](#instruction-tas-testandset).
|
||
|
||
Les Mutex restent un outil primitif. Ils ne s'appliquent pas sur des problèmes plus complexes.
|
||
|
||
# Sémaphore
|
||
Le sémaphore est une généralisation du mutex. Il permet de bloquer l'accès à une ressource partagée par plusieurs processus.
|
||
Il limite l'accès à plusieurs même ressources utilisées par plusieurs processus.
|
||
|
||
Par exemple : les processus ont besoin d'imprimer et il y a plusieurs imprimantes à disposition.
|
||
Le sémaphore va limiter l'accès au nombre d'imprimantes disponibles.
|
||
|
||
Les primitives de sémaphores sont :
|
||
- ***P*** (Passeren = passer) appelé aussi *wait*, *up*, *get* ou *lock*
|
||
```pascal
|
||
procedure P(var s:semaphore);
|
||
begin
|
||
s.num := s.num – 1;
|
||
if s.num < 0 then
|
||
block the process at the end of the list "s.waiting"
|
||
end if
|
||
end P;
|
||
```
|
||
- ***V*** (Vrijgeven = libérer) appelé aussi *signal*, *down*, *release* ou *unlock*
|
||
```pascal
|
||
procedure V(var s:semaphore);
|
||
begin
|
||
s.num := s.num + 1;
|
||
if s.num <= 0 then
|
||
unblock first process in the list "s.waiting"
|
||
end if
|
||
end V;
|
||
```
|
||
|
||
À l'initialisation, le sémaphore est initialisé au nombre de mêmes ressources disponibles.
|
||
Ou alors, il est initialisé à 1 pour un mutex.
|
||
Ou alors, il est initialisé à 0 pour un sémaphore binaire (file d'attente).
|
||
|
||
|
||
## Interblocage
|
||
L'interblocage est un problème qui peut survenir lorsqu'un ensemble de processus se bloquent mutuellement.
|
||
On appelle aussi cela un **DEADLOCK**.
|
||
|
||
## Résolution reader / writter
|
||
Le but est qu'il peut avoir plusieurs processus qui puisse lire la même ressource et aucun modifier la ressource.
|
||
Sinon, **un** processus peut modifier la ressource et aucun peut lire.
|
||
```cpp
|
||
void BeginRead(void) {
|
||
osSemaphoreAcquire(rw, osWaitForever);
|
||
osSemaphoreAcquire(mutexNbReader, osWaitForever); // ENTER CRITICAL SECTION
|
||
nbReader++;
|
||
if(nbReader == 1) osSemaphoreAcquire(w, osWaitForever);
|
||
osSemaphoreRelease(mutexNbReader); // EXIT CRITICAL SECTION
|
||
osSemaphoreRelease(rw);
|
||
}
|
||
|
||
void EndRead(void) {
|
||
osSemaphoreAcquire(mutexNbReader, osWaitForever); // ENTER CRITICAL SECTION
|
||
nbReader--;
|
||
if(nbReader == 0) osSemaphoreRelease(w);
|
||
osSemaphoreRelease(mutexNbReader); // EXIT CRITICAL SECTION
|
||
}
|
||
|
||
void BeginWrite(void) {
|
||
osSemaphoreAcquire(rw, osWaitForever);
|
||
osSemaphoreAcquire(w, osWaitForever);
|
||
}
|
||
|
||
void EndWrite(void) {
|
||
osSemaphoreRelease(w);
|
||
osSemaphoreRelease(rw);
|
||
}
|
||
```
|
||
|
||
|
||
|
||
|
||
# Synchronisation
|
||
|
||
|
||
Etat d'un processus :
|
||
|
||

|
||
|
||
Le processeur possède une liste de descripteur caractérisant chaque processus. Ces descripteurs de processus qui
|
||
contiennent le registre de status et un pointeur sur sa stack. Ils sont utilisé pour faire des switching context :
|
||
1. Le noyau va sauvegarder les registres de travail du processus en cours sur la stack.
|
||
2. Il va sauvegarder aussi le registre de status et le pointeur de stack dans la liste de descripteurs.
|
||
3. Il va prendre le descripteur du prochain processus et utiliser ses registres.
|
||
4. Le processus continue jusqu'au nouveau switching context.
|
||
|