mars
2012
Le modèle de programmation parallèle basé sur les tâches et la mémoire partagée est le plus répandu aujourd’hui, on le retrouve en particulier dans Java, C# et C avec OpenMP.
Variables partagées
Pour utiliser une variable dans plusieurs fonctions en parallèle, il faut la déclarer partagée. De cette façon elle peut être modifiée de façon atomique (sans état intermédiaire) par ces fonctions. Un exemple de déclaration est:
Share(<this: Personne>) externe
Il faut utiliser le service share
(voir passage de message) pour modifier la variable, par exemple:
.share!externe = inspecteur share!externe.Visite(site)
Il faut comprendre cela comme si le programme envoie des commandes à la variable partagée, en fait il n’attend pas que l’opération soit effectuée (ou cause une erreur).
L’envoi de commande peut produire un ou plusieurs résultats. Ces résultats deviennent disponibles au plus tard quand la méthode correspondante termine. D’ici là l’accès aux résultats est une opération bloquante.
Si une commande fait intervenir plusieurs variables dont la valeur n’est pas encore connue, elle commence à être évaluée seulement après que la valeur pour chacune d’elles est renseignée (progression en tout ou rien).
Les attributs de la variable partagée peuvent être accédés comme une variable normale. Si la variable a plusieurs états possibles, et son état courant ne donne pas accès à l’attribut en question, alors la tâche courante bloque en attendant que la variable change d’état.
Par exemple:
def Inspecteur = new Personne { __state incognito => # On ne révèle rien ici __state officiel => Matricule matricule } ... .share!externe = Inspecteur() ... Matricule m = externe.matricule
La tâche courante bloque jusqu’à ce que l’inspecteur sorte de son incognito et devienne officiel. Pour une variable normale, on obtiendrait immédiatement une erreur.
Le service share
garantit que les opérations qu’il gère s’exécutent dans l’ordre où elles apparaissent dans le programme (ordre séquentiel) pour une tâche donnée.
Le bloc fork
Si l’on veut poursuivre d’autres tâches en attendant qu’une variable soit renseignée, il faut mettre l’opération bloquante dans un bloc sync
ou fork
(les instructions d’un fork
sont comprises dans un block sync
implicite):
fork { Matricule m = externe.matricule # utilise m... } # reste des opérations...
Cela permet en particulier de créer des gestionnaires d’événement, dans le cas ci-dessus pour le changement d’état de l’inspecteur. Notez qu’un bloc fork
ne commence à s’exécuter que lorsque toutes les variables qu’il utilise sont renseignées. Si l’on ne veut pas attendre d’avoir une valeur pour une variable particulière, il faut utiliser un bloc fork
(ou sync
) imbriqué. Par exemple:
def _approuvé = share!principal.Approuve("Vérification du matricule inspecteur") fork { Matricule m = externe.matricule case: registre.contains(m) => def data = registre[m] # utilise data... else => fork: # Regarder si vérif approuvée ici seulement case if (_approuvé) { # Pas sur le registre! Faire quelque chose... } . } . }
On peut écrire un gestionnaire d’événements multiples avec la forme spéciale fork select
, qui s’apparente à un bloc case
où les instructions correspondantes s’exécutent dans un fork
quand la condition (garde) devient vraie. Le bloc fork select
continue de s’exécuter jusqu’à ce que le système devienne stable, c’est-à-dire que toutes les variables ont une valeur connue.
La variable qui signale la fin du programme est runtime.process.exit
. Si celle-ci est utilisée comme garde d’un fork select
, le gestionnaire d’événements s’exécute jusqu’à la fin du programme.
Partie 1 – Présentation
Partie 2 – Le passage de messages
Partie 4 – Les transactions