Les objets protégés en Ada
ou « Ada LoveRace condition »
Introduction¶
Dans un article précédent, Tâches et rendez-vous en Ada, nous avions présenté succinctement ce qu’était le tasking Ada et plus particulièrement le mécanisme de rendez-vous. Le fait que des tâches puissent s’exécuter en parallèle implique qu’elles puissent aussi accéder de façon concurrente à une donnée. Si nous n’y prenons pas garde, cette donnée peut alors devenir incohérente.
Race Condition¶
Une *race condition est le fait que le comportement d’une application soit dépendant de l’ordre d’exécution de ses tâches constituantes. Par exemple, si deux tâches accèdent en même temps à une même donnée, l’une la modifiant et l’autre la lisant, le résultat final sera probablement imprévisible.
Pour étayer notre propos, nous allons prendre l’exemple ci-dessous mettant à nouveau en scène nos deux compères R2D2 et C3PO. Ici, R2D2 et C3PO sont démarrés par le programme principal puis, chacun des deux robots vient respectivement toutes les ~100ms écrire et lire la variable globale Code_Imperial sans se soucier de l’autre et de la façon dont il accède à cette variable globale :
with Ada.Text_IO; use Ada.Text_IO;
procedure Test_LoveRace is
type Code_Imperial_T is array (1 .. 26) of Character
with Default_Component_Value => '-';
Code_Imperial : Code_Imperial_T;
task R2D2 is
entry Demarrer;
end R2D2;
task C3PO is
entry Demarrer;
end C3PO;
task body R2D2 is
begin
accept Demarrer;
for I in Code_Imperial_T'Range loop
Code_Imperial (I) := Character'Val (I + 64); --> on modifie le « Code_Imperial »
delay 0.1;
end loop;
end R2D2;
task body C3PO is
begin
accept Demarrer;
Put ("Maître Luke, le code impérial est : ");
for I in Code_Imperial_T'Range loop
Put (Code_Imperial (I)); --> on lit le « Code_Imperial »
delay 0.1;
end loop;
New_Line;
end C3PO;
begin
R2D2.Demarrer;
C3PO.Demarrer;
end Test_LoveRace;
Si on exécute notre programme, on aura un résultat non déterministe et un code impérial inutilisable pour maître Luke !
Maître Luke, le code impérial est : AB-E-GH-J-M------R----W-Y-
Peut-on remédier simplement au problème pour que le code fourni à maître Luke soit complet ?
La première solution serait de faire en sorte que C3PO ne lise le Code_Imperial qu’une fois que R2D2 ait terminé de l’écrire :
with …
procedure Test_LoveRace is
…
task body R2D2 is
begin
accept Demarrer;
for I in Code_Imperial_T'Range loop
…
end loop;
C3PO.Demarrer; --> R2D2 ne va plus modifier « Code_Imperial »
-- on peut donc démarrer C3PO
end R2D2;
task body C3PO is
begin
accept Demarrer;
…
end C3PO;
begin
R2D2.Demarrer;
-- C3PO.Demarrer;
end Test_LoveRace;
Cela fonctionne, et nous aurions bien systématiquement :
Maître Luke, le code impérial est : ABCDEFGHIJKLMNOPQRSTUVWXYZ
Mais, dans ce cas, on se ramène à une application séquentielle, et cela n’est pas ce que l’on veut !
On veut pouvoir disposer du code impérial au fur et à mesure que celui-ci est produit par R2D2. Nous allons donc devoir mettre en œuvre un mécanisme permettant de synchroniser R2D2 et C3PO.
En Ada83, la solution aurait-été d’utiliser une tâche tierce mais cela est peu adapté car trop orienté traitements. C’est entre autres pour cette raison qu’Ada95 a introduit les objets protégés qui sont plus adaptés car orientés données.
C’est quoi un objet protégé en Ada ?¶
Les objets protégés encapsulent des données qui ne sont accessibles que par des opérations protégées que sont les entrées, les procédures et les fonctions. Ils sont basés sur le concept de moniteur spécifié par Hoare et Brinch Hansen en 1974. Un mécanisme de verrou permet que les opérations protégées soient exécutées de façon à garantir la modification des données en exclusion mutuelle.
Contrairement aux tâches, ce sont des objets passifs ne nécessitant pas de changement de contexte. Ils se caractérisent aussi par leur efficacité et leur simplicité.
Nous allons encore une fois filer la métaphore et considérer un objet protégé comme un coffre-fort ayant plusieurs portes. Un mécanisme garantit que, si vous ouvrez une porte, alors les autres sont automatiquement et systématiquement verrouillées jusqu’à ce que vous refermiez celle-ci.
Il y a trois types de portes (ce sont les opérations protégées) :
- Les procédures : il n’y a pas de serrure, on peut ouvrir le coffre-fort quand on veut (pourvu qu’aucune autre porte ne soit ouverte) et modifier le contenu du coffre-fort.
- Les fonctions : il n’y a pas de serrure, on peut ouvrir le coffre-fort quand on veut (pourvu qu’aucune autre porte ne soit ouverte) mais on ne peut que regarder le contenu du coffre-fort.
- Les entrées : il y a une serrure basée sur une condition. Si la condition est vraie, la serrure est ouverte. Dans ce cas, si aucune autre porte n’est ouverte, vous pouvez ouvrir la porte. Comme pour les procédures, vous pouvez modifier le contenu du coffre-fort. Quand vous fermerez la porte, et si vous avez modifié le contenu du coffre-fort, alors toutes les conditions des serrures seront réévaluées ce qui implique que certaines serrures pourront se fermer et d’autres s’ouvrir. Si vous ne pouvez pas ouvrir la porte (la condition de la serrure est fausse) alors vous êtes mis dans une file d’attente devant la porte. Lors de la réévaluation des conditions des serrures, les tâches en file d’attente dont la serrure est maintenant ouverte, peuvent accéder à l’objet protégé. Cette réévaluation est aussi faite pour les procédures dans le cas d’une modification du contenu du coffre-fort. Dans les objets protégés, la condition permettant d’ouvrir la serrure s’appelle une barrière.
Le schéma ci-dessous présente l’objet protégé qui va être utilisé pour que nos deux compères optimisent au mieux l’écriture et la lecture du code impérial.
Le coffre-fort (buffer) contiendra :
- Un tableau de 26 lettres devant contenir le code impérial : ABC…Z.
- Un compteur indiquant le nombre de lettres du code impérial disponibles et compris entre 0 et 26.
- Un index d’écriture dans le tableau compris entre 1 et 26.
- Un index de lecture dans le tableau compris entre 1 et 26.
Les quatre éléments ci-dessus sont inaccessibles de l’extérieur, la seule façon de les modifier est d’ouvrir une des deux portes (entrées) Lire ou Ecrire mais une seule à la fois.
-
L’entrée Ecrire : c’est une porte ne pouvant être ouverte que si le compteur est inférieur à 26. Une fois ouverte, la porte permet de modifier :
- Le compteur (+1).
- Le tableau.
- L’index d’écriture (+1).
-
L’entrée Lire : c’est une porte ne pouvant être ouverte que si le compteur est supérieur à 0. Une fois ouverte, la porte permet de lire le tableau et de modifier :
- Le compteur (-1).
- L’index de lecture (+1).
Voyons maintenant l’implémentation Ada de notre objet protégé :
with Ada.Text_IO; use Ada.Text_IO;
procedure Test_LoveRace is
type Code_Imperial_T is array (1 .. 26) of Character
with Default_Component_Value => '-';
-- Code_Imperial : Code_Imperial_T; --> nous n'avons plus besoin d'une donnée globale et
-- partagée
protected Buffer is --> le coffre-fort avec ses deux portes
entry Ecrire (Data : in Character);
entry Lire (Data : out Character);
private
--> les données protégées
Data_Interne : Code_Imperial_T;
Compteur : Natural range 0 .. Code_Imperial_T'Last := 0;
Index_Ecriture,
Index_Lecture : Positive range Code_Imperial_T'First .. Code_Imperial_T'Last
:= Code_Imperial_T'First;
end Buffer;
protected body Buffer is
entry Ecrire (Data : in Character)
when Compteur < Code_Imperial_T'Length is --> barrière permettant d'accéder
-- à l'objet protégé
begin
Data_Interne (Index_Ecriture) := Data;
if Index_Ecriture = Code_Imperial_T'Last then
Index_Ecriture := Code_Imperial_T'First;
else
Index_Ecriture := @ + 1;
end if;
Compteur := @ + 1;
end Ecrire;
entry Lire (Data : out Character)
when Compteur > 0 is --> barrière permettant d'accéder
-- à l'objet protégé
begin
Data := Data_Interne (Index_Lecture);
if Index_Lecture = Code_Imperial_T'Last then
Index_Lecture := Code_Imperial_T'First;
else
Index_Lecture := @ + 1;
end if;
Compteur := @ - 1;
end Lire;
end Buffer;
task R2D2 is
entry Demarrer;
end R2D2;
task C3PO is
entry Demarrer;
end C3PO;
task body R2D2 is
begin
accept Demarrer;
for I in Code_Imperial_T'Range loop
-- Code_Imperial (I) := Character'Val (I + 64);
Buffer.Ecrire (Data => Character'Val (I + 64)); --> si la barrière est fermée alors
-- R2D2 sera mis dans la file de
-- l'entrée Ecrire
delay 0.1;
end loop;
end R2D2;
task body C3PO is
Code_Imperial_Local : Code_Imperial_T; --> notre code impérial est maintenant local
begin
accept Demarrer;
Put ("Maître Luke, le code impérial est : ");
for I in Code_Imperial_T'Range loop
Buffer.Lire (Data => Code_Imperial_Local (I)); --> si la barrière est fermée alors
-- C3PO sera mis dans la file de
-- l'entrée Lire
Put (Code_Imperial_Local (I));
delay 0.1;
end loop;
New_Line;
end C3PO;
begin
R2D2.Demarrer;
C3PO.Demarrer;
end Test_LoveRace;
L’exécution de notre programme sera maintenant correcte :
Maître Luke, le code impérial est : ABCDEFGHIJKLMNOPQRSTUVWXYZ
Conclusion¶
Les Races Conditions sont un des problèmes courants de la programmation multi-thread, celui-ci peut être adressé en utilisant des techniques de synchronisation et d’exclusion mutuelle appropriées au travers des objets protégés.
Comme pour les tâches, ce sont des structures natives au langage offrant un haut niveau d’abstraction et masquant les détails complexes de l’implémentation. Ils garantissent l’exclusion mutuelle simplement, tout en s’affranchissant aussi d’un autre problème qu’est l’inversion de priorité.
En résumé, les objets protégés offrent un mécanisme puissant et sûr pour la programmation concurrente, ce qui les rend particulièrement efficaces et pertinents dans les applications critiques où la fiabilité et la sécurité sont primordiales.
