Tâches et rendez-vous en Ada
ou « Ada et les rendez-vous galants »
Introduction¶
Le tasking Ada offre la possibilité d’exécuter des portions de code de façon indépendante et au sein d’un même programme. Ce paradigme de la programmation concurrente permet à plusieurs tâches d’être exécutées simultanément ou de manière entrelacée.
En Ada, une tâche est un objet de haut niveau ayant une sémantique précise. Dans la pratique, une tâche sera implémentée sous la forme d’un thread.
L’objet de cet article est de faire une première introduction vous permettant d’appréhender ce qu’est le « tasking Ada » et ce qu’il permet de faire.
C’est quoi une tâche en Ada ?¶
En Ada, une tâche est un objet de haut niveau modélisant un processeur logique. Il y a une complète abstraction par rapport à :
- L’implémentation effective de la tâche (thread ou autre).
- L’architecture matérielle (multi-cœurs ou autre).
Les tâches sont des éléments coopératifs d’un programme s’exécutant dans un même espace d’adressage.
On peut avoir des tâches singletons ou des types tâches permettant par exemple de créer des tableaux de tâches. Dans la suite de l’article, et par souci de concision, nous ne manipulerons que des tâches singletons.
Même si cela n’est pas obligatoirement nécessaire, une tâche doit pouvoir interagir avec ses « congénères ». Cela se fait par le biais de rendez-vous. Celui-ci permet la synchronisation et l’échange éventuel de données entre tâches. Pour ce faire, une tâche déclare ce que l’on nomme des entrées. La tâche acceptera un rendez-vous au travers de l’acceptance de ce dernier. Une fois le rendez-vous terminé, les deux tâches reprennent leur exécution propre.
Le rendez-vous est un mécanisme orienté traitement. Dans un prochain article nous verrons, au travers des objets protégés, comment avoir un mécanisme orienté données.
Prenons un premier exemple :
with Ada.Text_IO; use Ada.Text_IO;
procedure Star_Wars is
task R2D2 is
entry Demarrer; --> entrées des RDV possibles
entry Lire (Val : in Integer);
end R2D2;
task C3PO is
entry Demarrer; --> entrées des RDV possibles
entry Fournir (Coordonnees : in Integer);
end C3PO;
task body R2D2 is
Val1, Val2 : Integer;
function Calculer (V1, V2 : in Integer) return Integer is (V1 + V2);
begin
Put_Line ("1> R2D2 est activé");
accept Demarrer; --> acceptance de RDV
Put_Line ("2> R2D2 est démarré");
Put_Line ("3> R2D2 attend la première valeur...");
accept Lire (Val : in Integer) do
Put_Line ("4> ***RDV R2D2");
Val1 := Val;
end Lire;
Put_Line ("5> R2D2 attend la deuxième valeur...");
accept Lire (Val : in Integer) do
Put_Line ("6> ***RDV R2D2");
Val2 := Val;
end Lire;
Put_Line ("7> R2D2 calcule les coordonnées et les fournit à C3PO");
C3PO.Fournir (Coordonnees => Calculer (V1 => Val1,
V2 => VAl2));
Put_Line ("8> R2D2 s'est terminé");
end R2D2;
task body C3PO is
begin
Put_Line ("1>> C3PO est activé");
accept Demarrer; --> acceptance de RDV
Put_Line ("2>> C3PO est démarré");
Put_Line ("3>> C3PO fournit la première valeur à R2D2");
R2D2.Lire (Val => 1);
Put_Line ("4>> C3PO fournit la deuxième valeur à R2D2");
R2D2.Lire (Val => 2);
Put_Line ("5>> C3PO attend les coordonnées de la flotte impériale...");
accept Fournir (Coordonnees : in Integer) do
Put_Line ("6>> ***RDV C3PO");
Put_Line ("7>> Maître Luke, la flotte impériale est à ces coordonnées :" &
Coordonnees'Image);
end Fournir;
Put_Line ("8>> C3PO s'est terminé");
end C3PO;
begin
R2D2.Demarrer;
C3PO.Demarrer;
end Star_Wars;
Ici nous avons la tâche R2D2 et la tâche C3PO. La tâche R2D2 définit deux entrées. Une pour être démarrée et l’autre pour lire des valeurs lui permettant de calculer les coordonnées de la flotte impériale. Dans le corps de la tâche R2D2, on constate que l’entrée Lire a deux acceptances. Cela veut donc dire que la tâche R2D2 exécute le rendez-vous Lire en deux points de son implémentation.
La tâche C3PO définit aussi deux entrées. Une pour être démarrée et l’autre pour fournir à maitre Luke les coordonnées de la flotte impériale calculées par R2D2. Contrairement à RD2D, C3PO exécute le rendez-vous Fournir en un seul point de son implémentation.
Le programme principal ne fait que démarrer les deux robots qui échangent alors des données.
On remarque que les acceptances Demarer n’ont pas d’implémentation. Ce sont de simples points de synchronisation.
L’exécution produit :
1> R2D2 est activé
1>> C3PO est activé
2> R2D2 est démarré
3> R2D2 attend la première valeur...
2>> C3PO est démarré
3>> C3PO fournit la première valeur à R2D2
4> ***RDV R2D2
5> R2D2 attend la deuxième valeur...
4>> C3PO fournit la deuxième valeur à R2D2
6> ***RDV R2D2
7> R2D2 calcule les coordonnées et les fournit à C3PO
5>> C3PO attend les coordonnées de la flotte impériale...
6>> ***RDV C3PO
7>> Maître Luke, la flotte impériale est à ces coordonnées : 3
8>> C3PO s'est terminé
8> R2D2 s'est terminé
Deux remarques :
- Dans la pratique, une tâche intègre le plus souvent un schéma itératif qui fait qu’elle ne se termine jamais ou du moins sous certaines conditions. Prenons l’exemple de R2D2, son implémentation devrait ressembler plus au code ci-dessous sachant que dans ce cas, R2D2 ne s’arrêterait jamais !
task body R2D2 is
Val1, Val2 : Integer;
function Calculer (V1, V2 : in Integer) return Integer is (V1 + V2);
begin
Put_Line ("1> R2D2 est activé");
accept Demarrer; --> acceptance de RDV
Put_Line ("2> R2D2 est démarré");
loop --> schéma itératif (début)
Put_Line ("3> R2D2 attend la première valeur...");
accept Lire (Val : in Integer) do
Put_Line ("4> ***RDV R2D2");
Val1 := Val;
end Lire;
Put_Line ("5> R2D2 attend la deuxième valeur...");
accept Lire (Val : in Integer) do
Put_Line ("6> ***RDV R2D2");
Val2 := Val;
end Lire;
Put_Line ("7> R2D2 calcule les coordonnées et les fournit à C3PO");
C3PO.Fournir (Coordonnees => Calculer (V1 => Val1,
V2 => VAl2));
end loop; --> schéma itératif (fin)
end R2D2;
- Dans le code actuel, C3PO attend la réponse de R2D2 sans se soucier du temps que cela peut prendre. Mais si R2D2 met trop de temps à fournir les coordonnées de la flotte impériale, ne serait-il pas préférable de ne pas bloquer C3PO et d’en avertir maître Luke ?
Des rendez-vous à la carte¶
Au travers de l’instruction select, il est possible de raffiner les rendez-vous.
Par exemple, vous pouvez :
- Définir le comportement et le degré de patience des tâches quand un rendez-vous se fait trop tôt ou trop tard.
- N’accepter un rendez-vous que sous certaines conditions.
- …
Dans le cadre de cet article, nous ne présenterons qu’une seule utilisation de l’instruction select en faisant en sorte que C3PO n’attende pas plus d’une heure son rendez-vous avec R2D2.
Pour cela, nous allons écrire le code suivant où nous avons mis en exergue ce qui a réellement changé.
La sémantique est la suivante : si C3PO attend plus d’une heure son rendez-vous avec R2D2, alors, il n’attend plus et il prévient maître Luke.
with …
procedure Star_Wars is
…
task body R2D2 is
…
begin
…
C3PO.Lire (Coordonnees => Calculer (…)); -- le calcul « hyper-spatial » des coordonnées
-- prend plus d’une heure…
…
end R2D2;
task body C3PO is
begin
Put_Line ("1>> C3PO est activé");
…
Put_Line ("5>> C3PO attend les coordonnées de la flotte impériale...");
select --> attente du RDV sous condition
accept Fournir (Coordonnees : in Integer) do
Put_Line ("6>> ***RDV C3PO");
Put_Line ("7>> Maître Luke, la flotte impériale est à ces coordonnées :" &
Coordonnees'Image);
end Fournir;
or
delay 60.0 * 60.0; --> on est disposé à attendre une heure mais pas plus !
Put_Line ("6>> ***RDV trop tardif !");
Put_Line ("7>> Maître Luke, je n'ai toujours pas les coordonnées de la flotte
impériale !");
end select;
Put_Line ("8>> C3PO s'est terminé");
end C3PO;
begin
…
end Star_Wars;
Donc, si R2D2 peine à calculer les coordonnées de la flotte impériale, le résultat de l’exécution sera alors :
1> R2D2 est activé
1>> C3PO est activé
2> R2D2 est démarré
3> R2D2 attend la première valeur...
2>> C3PO est démarré
3>> C3PO fournit la première valeur à R2D2
4> ***RDV R2D2
5> R2D2 attend la deuxième valeur...
4>> C3PO fournit la deuxième valeur à R2D2
6> ***RDV R2D2
7> R2D2 calcule les coordonnées et les fournit à C3PO
5>> C3PO attend les coordonnées de la flotte impériale...
6>> ***RDV trop tardif !
7>> Maître Luke, je n'ai toujours pas les coordonnées de la flotte impériale !
8>> C3PO s'est terminé
8> R2D2 s'est terminé (après plus d’une heure…)
Conclusion¶
Nous avons eu un premier aperçu de ce qu’est le tasking en Ada au travers du mécanisme de rendez-vous. Dans un prochain article, nous verrons ce qu’implique les accès concurrents inhérents au tasking et comment adresser simplement et efficacement ces derniers au travers des objets protégés.
Le tasking Ada est un trait essentiel pour les systèmes temps réel. Il a des capacités reconnues pour son efficacité dans les systèmes critiques où la fiabilité et le déterminisme sont primordiaux.
Référence¶
[1] : https://learn.adacore.com/courses/intro-to-ada/chapters/tasking.html
