Les tâches
L'exécution d'un programme ne contenant pas de tâche est définie en termes d'exécution séquentielle de ses actions, selon les règles décrites dans d'autres pages. Ces actions peuvent être considérées comme exécutées par un seul processeur logique.
Les tâches sont des entités dont les exécutions se déroulent en parallèle dans le sens suivant. Chaque tâche peut être considérée comme exécutée par un processeur logique lui étant propre. Différentes tâches (différents processeurs logiques) se déroulent indépendamment, sauf aux points où elles se synchronisent.
Certaines tâches ont des entrées. Une entrée d'une tâche peut être appelée par d'autres tâches. Une tâche accepte un appel d'une de ses entrées en exécutant une instruction accept pour l'entrée. La synchronisation est réalisée par rendez-vous entre une tâche émettant un appel d'entrée et une tâche acceptant l'appel. Certaines entrées ont des paramètres ; les appels d'entrée et les instructions accept pour ces entrées sont les principaux moyens de communication de valeurs entre les tâches.
Les propriétés de chaque tâche sont définies par une unité de tâche correspondante se composant d'une spécification de tâche et d'un corps de tâche. Les unités de tâche sont l'une des quatre formes d'unité de programme dont les programmes peuvent être composés. Les autres formes sont les sous-programmes, les paquets et les unités génériques. Les propriétés des unités de tâches, des tâches et des entrées, ainsi que les instructions affectant l'interaction entre les tâches (c'est-à-dire les instructions d'appel d'entrée, les instructions d'acceptation, les instructions de délai, les instructions de sélection et les instructions d'abandon) sont décrites dans cette page.
Remarque : les tâches parallèles (processeurs logiques parallèles) peuvent être implémentées sur des multi-ordinateurs, des multiprocesseurs ou avec une exécution entrelacée sur un seul processeur physique. D'autre part, chaque fois qu'une implémentation peut détecter que le même effet peut être garanti si des parties des actions d'une tâche donnée sont exécutées par différents processeurs physiques agissant en parallèle, elle peut choisir de les exécuter de cette manière; dans un tel cas, plusieurs processeurs physiques implémentent un seul processeur logique.
Spécifications de tâches et corps de tâches
Une unité de tâche se compose d'une spécification de tâche et d'un corps de tâche. Une spécification de tâche commençant par les mots réservés type de tâche déclare un type de tâche. La valeur d'un objet d'un type de tâche désigne une tâche ayant les entrées, le cas échéant, étant déclarées dans la spécification de tâche; ces entrées sont également appelées entrées de cet objet. L'exécution de la tâche est définie par le corps de tâche correspondant.
Une spécification de tâche sans le mot réservé type définit une tâche unique. Une déclaration de tâche avec cette forme de spécification équivaut à la déclaration d'un type de tâche anonyme immédiatement suivie de la déclaration d'un objet du type de tâche, et l'identifiant de l'unité de tâche nomme l'objet. Dans la suite de cette page, les explications sont données en termes de déclarations de type de tâche; les explications correspondantes pour les déclarations de tâche unique découlent de l'équivalence indiquée :
declaration_de_tache ::= specification_de_tache; specification_de_tache ::= task [type] identifiant [is [declaration_d_entree] [clause_de_representation] end [tache_simple_nom]] tache_corps ::= task body tache_simple_nom is [ partie_declarative] begin sequence_d_instructions [ exception gestionnaire_d_exceptions | gestionnaire_d_exceptions]] end [tache_simple_nom]; |
Le nom simple au début d'un corps de tâche doit répéter l'identifiant de l'unité de tâche. De même, si un nom simple apparaît à la fin de la spécification ou du corps de tâche, il doit répéter l'identifiant de l'unité de tâche. Dans un corps de tâche, le nom de l'unité de tâche correspondante peut également être utilisé pour faire référence à l'objet tâche qui désigne la tâche qui exécute actuellement le corps; de plus, l'utilisation de ce nom comme marque de type n'est pas autorisée dans l'unité de tâche elle-même.
Pour l'élaboration d'une spécification de tâche, les déclarations d'entrée et les clauses de représentation, le cas échéant, sont élaborées dans l'ordre indiqué. De telles clauses de représentation ne s'appliquent qu'aux entrées déclarées dans la spécification de tâche.
L'élaboration d'un corps de tâche n'a d'autre effet que d'établir que le corps peut désormais être utilisé pour l'exécution de tâches désignées par des objets du type de tâche correspondant. L'exécution d'un corps de tâche est invoquée par l'activation d'un objet tâche du type correspondant. Les gestionnaires d'exceptions facultatifs à la fin d'un corps de tâche gèrent les exceptions levées pendant l'exécution de la séquence d'instructions du corps de tâche.
Exemples de spécifications de types de tâches :
Exemples de spécifications de tâches individuelles :
Exemple de tâches des charges et de corps correspondant :
- task PROTECTED_ARRAY is
- -- INDEX et ITEM sont des types globaux
- entry READ (N : in INDEX; V : out ITEM);
- entry WRITE (N : in INDEX; E : in ITEM);
- end;
-
- task body PROTECTED_ARRAY is
- TABLE : array(INDEX) of ITEM := (INDEX => NULL_ITEM);
- begin
- loop
- select
- accept READ (N : in INDEX; V : out ITEM) do
- V := TABLE(N);
- end READ;
- or
- accept WRITE(N : in INDEX; E : in ITEM) do
- TABLE(N) := E;
- end WRITE;
- end select;
- end loop;
- end PROTECTED_ARRAY;
Remarque : Une spécification de tâche spécifie l'interface des tâches du type de tâche avec d'autres tâches du même type ou de types différents, ainsi qu'avec le programme principal.
Types de tâches et objets de tâches
Un type de tâche est un type limité. Par conséquent, ni l'affectation ni la comparaison prédéfinie pour l'égalité et l'inégalité ne sont définies pour les objets de type tâche; de plus, le mode out n'est pas autorisé pour un paramètre formel dont le type est un type tâche.
Un objet tâche est un objet dont le type est un type de tâche. La valeur d'un objet tâche désigne une tâche possédant les entrées du type de tâche correspondant et dont l'exécution est spécifiée par le corps de tâche correspondant. Si un objet tâche est l'objet, ou un sous-composant de l'objet, déclaré par une déclaration d'objet, alors la valeur de l'objet tâche est définie par l'élaboration de la déclaration d'objet. Si un objet tâche est l'objet, ou une sous-composante de l'objet, créé par l'évaluation d'un allocateur, alors la valeur de l'objet tâche est définie par l'évaluation de l'allocateur. Pour tous les modes de paramètres, si un paramètre réel désigne une tâche, le paramètre formel associé désigne la même tâche; il en va de même pour un sous-composante d'un paramètre réel et le sous-composante correspondant du paramètre formel associé; enfin, il en va de même pour les paramètres génériques.
Exemples :
- CONTROL : RESOURCE;
- TELETYPE : KEYBOARD_DRIVER;
- POOL : array(1 .. 10) of KEYBOARD_DRIVER;
- -- voir aussi des exemples de déclarations de tâches uniques
Exemple de type d'accès désignant des objets de tâche :
- type KEYBOARD is access KEYBOARD_DRIVER;
-
- TERMINAL : KEYBOARD := new KEYBOARD_DRIVER;
Remarques : Étant donné qu'un type de tâche est un type limité, il peut apparaître comme la définition d'un type privé limité dans une partie privée, et comme un paramètre réel générique associé à un paramètre formel dont le type est un type limité. En revanche, le type d'un paramètre formel générique de mode in ne doit pas être un type limité et ne peut donc pas être un type de tâche.
Les objets tâche se comportent comme des constantes (un objet tâche désigne toujours la même tâche) puisque leurs valeurs sont définies implicitement soit à la déclaration ou à l'allocation, soit par une association de paramètres, et puisqu'aucune affectation n'est disponible. Cependant, le mot réservé constant n'est pas autorisé dans la déclaration d'un objet tâche car cela nécessiterait une initialisation explicite. Un objet tâche étant un paramètre formel de mode in est une constante (comme tout paramètre formel de ce mode).
Si une application doit stocker et échanger des identités de tâches, elle peut le faire en définissant un type d'accès désignant les objets tâche correspondants et en utilisant des valeurs d'accès à des fins d'identification (voir l'exemple ci-dessus). L'affectation est disponible pour un tel type d'accès comme pour tout type d'accès.
Les déclarations de sous-types sont autorisées pour les types de tâches comme pour les autres types, mais il n'y a aucune contrainte applicable aux types de tâches.
Exécution de tâche - Activation de tâche
Un corps de tâche définit l'exécution de toute tâche désignée par un objet tâche du type de tâche correspondant. La partie initiale de cette exécution est appelée activation de l'objet tâche, ainsi que celle de la tâche désignée; elle consiste en l'élaboration de la partie déclarative, le cas échéant, du corps de tâche. L'exécution de différentes tâches, en particulier leur activation, se déroule en parallèle. Si une déclaration d'objet déclarant un objet tâche se produit immédiatement dans une partie déclarative, alors l'activation de l'objet tâche commence après l'élaboration de la partie déclarative (c'est-à-dire après avoir passé le mot réservé begin après la partie déclarative); de même, si une telle déclaration se produit immédiatement dans une spécification de paquet, l'activation commence après l'élaboration de la partie déclarative du corps de paquet. Il en va de même pour l'activation d'un objet tâche étant une sous-composante d'un objet déclaré immédiatement dans une partie déclarative ou une spécification de paquet. La première instruction suivant la partie déclarative n'est exécutée qu'après la conclusion de l'activation de ces objets tâche.
Si une exception est levée par l'activation d'une de ces tâches, cette tâche devient une tâche terminée; les autres tâches ne sont pas directement affectées. Si l'une de ces tâches devient ainsi terminée pendant son activation, l'exception TASKING_ERROR est levée à la fin de l'activation de toutes ces tâches (avec succès ou non) ; l'exception est levée à un endroit qui se trouve immédiatement avant la première instruction suivant la partie déclarative (immédiatement après le mot réservé begin). Si plusieurs de ces tâches deviennent ainsi terminées pendant leur activation, l'exception TASKING_ERROR n'est levée qu'une seule fois.
Si une exception est levée par l'élaboration d'une partie déclarative ou d'une spécification de paquet, alors toute tâche créée (directement ou indirectement) par cette élaboration et qui n'est pas encore activée devient terminée et n'est donc jamais activée.
Pour les règles ci-dessus, dans tout corps de paquet sans instructions, une instruction null est supposée. Pour tout paquet sans corps de paquet, un corps de paquet implicite contenant une seule instruction null est supposé. Si un paquet sans corps de paquet est déclaré immédiatement dans une unité de programme ou une instruction de bloc, le corps de paquet implicite apparaît à la fin de la partie déclarative de l'unité de programme ou de l'instruction de bloc; s'il existe plusieurs paquets de ce type, l'ordre des corps de paquet implicites n'est pas défini.
Un objet tâche étant l'objet, ou un sous-composante de l'objet, créé par l'évaluation d'un allocateur est activé par cette évaluation. L'activation commence après toute initialisation de l'objet créé par l'allocateur ; si plusieurs sous-composantes sont des objets tâche, ils sont activés en parallèle. La valeur d'accès désignant un tel objet n'est renvoyée par l'allocateur qu'après la conclusion de ces activations.
Si une exception est levée par l'activation de l'une de ces tâches, cette tâche devient une tâche terminée ; les autres tâches ne sont pas directement affectées. Si l'une de ces tâches devient ainsi terminée pendant son activation, l'exception TASKING_ERROR est levée à la fin de l'activation de toutes ces tâches (avec succès ou non); l'exception est levée à l'endroit où l'allocateur est évalué. Si plusieurs de ces tâches sont ainsi terminées lors de leur activation, l'exception TASKING_ERROR n'est levée qu'une seule fois.
Si une exception est levée par l'initialisation de l'objet créé par un allocateur (donc avant le début de toute activation), toute tâche désignée par une sous-composante de cet objet devient terminée et n'est donc jamais activée.
Exemple :
Remarques : Une entrée d'une tâche peut être appelée avant que la tâche n'ait été activée. Si plusieurs tâches sont activées en parallèle, l'exécution de l'une de ces tâches n'a pas besoin d'attendre la fin de l'activation des autres tâches. Une tâche peut être terminée pendant son activation soit à cause d'une exception, soit parce qu'elle est interrompue.
Dépendance des tâches - Fin des tâches
Chaque tâche dépend d'au moins un maître. Un maître est une construction étant soit une tâche, soit une instruction de bloc ou un sous-programme en cours d'exécution, soit un paquet de bibliothèque (un paquet déclaré dans une autre unité de programme n'est pas un maître). La dépendance à un maître est une dépendance directe dans les deux cas suivants :
- La tâche désignée par un objet tâche étant l'objet, ou un sous-composante de l'objet, créé par l'évaluation d'un allocateur dépend du maître élaborant la définition de type d'accès correspondante.
- La tâche désignée par tout autre objet tâche dépend du maître dont l'exécution crée l'objet tâche.
De plus, si une tâche dépend d'un maître donné qui est une instruction de bloc exécutée par un autre maître, alors la tâche dépend aussi de cet autre maître, de manière indirecte; il en va de même si le maître donné est un sous-programme appelé par un autre maître, et si le maître donné est une tâche dépendant (directement ou indirectement) d'un autre maître. Des dépendances existent pour les objets d'un type privé dont la déclaration complète est en termes de type de tâche.
On dit qu'une tâche a terminé son exécution lorsqu'elle a terminé l'exécution de la séquence d'instructions qui apparaît après le mot réservé begin dans le corps correspondant. De même, on dit qu'un bloc ou un sous-programme a terminé son exécution lorsqu'il a terminé l'exécution de la séquence d'instructions correspondante. Pour une instruction de bloc, l'exécution est également dite terminée lorsqu'elle atteint une instruction exit, return ou goto transférant le contrôle hors du bloc. Pour une procédure, l'exécution est également dite terminée lorsqu'une instruction return correspondante est atteinte. Pour une fonction, l'exécution est également dite terminée après l'évaluation de l'expression de résultat d'une instruction return. Enfin, l'exécution d'une tâche, d'une instruction de bloc ou d'un sous-programme est terminée si une exception est levée par l'exécution de sa séquence d'instructions et qu'il n'y a pas de gestionnaire correspondant, ou, s'il y en a un, lorsqu'elle a terminé l'exécution du gestionnaire correspondant.
Si une tâche n'a pas de tâche dépendante, sa terminaison a lieu lorsque son exécution est terminée. Après sa terminaison, une tâche est dite terminée. Si une tâche a des tâches dépendantes, sa terminaison a lieu lorsque l'exécution de la tâche est terminée et que toutes les tâches dépendantes sont terminées. Une instruction de bloc ou un corps de sous-programme dont l'exécution est terminée n'est pas quitté tant que toutes ses tâches dépendantes ne sont pas terminées.
La terminaison d'une tâche a lieu si et seulement si son exécution a atteint une alternative de terminaison ouverte dans une instruction select, et les conditions suivantes sont satisfaites :
- La tâche dépend d'un maître dont l'exécution est terminée (et donc pas d'un paquet de bibliothèque).
- Chaque tâche dépendant du maître considéré est soit déjà terminée, soit en attente d'une alternative de terminaison ouverte d'une instruction select.
Lorsque les deux conditions sont remplies, la tâche considérée devient terminée, ainsi que toutes les tâches qui dépendent du maître considéré.
Exemple :
- declare
- type GLOBAL is access RESOURCE;
- A, B : RESOURCE;
- G : GLOBAL;
- begin
- -- activation of A and B
- declare
- type LOCAL is access RESOURCE;
- X : GLOBAL := new RESOURCE; -- activation de X.all
- L : LOCAL := new RESOURCE; -- activation de L.all
- C : RESOURCE;
- begin
- activation of C
- G := X; -- G et X désignent tous deux le même objet de tâche
- end; -- attendre la fin de C et L.all (mais pas X.all)
- end; -- attendre la fin de A, B et G.all
Remarques : Les règles données pour la terminaison impliquent que toutes les tâches dépendant (directement ou indirectement) d'un maître donné et n'étant pas déjà terminées, peuvent être terminées (collectivement) si et seulement si chacune d'entre elles attend une alternative de terminaison ouverte d'une instruction select et que l'exécution du maître donné est terminée.
Les règles habituelles s'appliquent au programme principal. Par conséquent, la terminaison du programme principal attend la terminaison de toute tâche dépendante même si le type de tâche correspondant est déclaré dans un paquetage de bibliothèque. D'autre part, la terminaison du programme principal n'attend pas la terminaison des tâches dépendant des paquets de bibliothèque; le langage ne définit pas si de telles tâches doivent se terminer.
Pour un type d'accès dérivé d'un autre type d'accès, la définition du type d'accès correspondante est celle du type parent; la dépendance est sur le maître élaborant la définition du type d'accès parent ultime.
Une déclaration de renommage définit un nouveau nom pour une entité existante et ne crée donc aucune dépendance supplémentaire.
Entrées, appels d'entrée et instructions Accept
Les appels d'entrée et les instructions Accept sont les principaux moyens de synchronisation des tâches et de communication des valeurs entre les tâches. Une déclaration d'entrée est similaire à une déclaration de sous-programme et n'est autorisée que dans une spécification de tâche. Les actions à effectuer lorsqu'une entrée est appelée sont spécifiées par les instructions Accept correspondantes.
declaration_d_entree ::= entry identifiant [(intervalle_discrete)] [partie_formelle]; instruction_d_appel_d_entree ::= nom_entree [partie_parametre_actuel]; declaration_d_acceptation ::= accept entree_simple_nom [(index_des_entrees)] [partie_formelle] [do sequence_d_instructions end [entree_simple_nom]]; index_des_entrees ::= expression |
Une déclaration d'entrée incluant un intervalle discret déclare une famille d'entrées distinctes ayant la même partie formelle (le cas échéant); c'est-à-dire une telle entrée pour chaque valeur de la plage discrète. Le terme entrée unique est utilisé dans la définition de toute règle s'appliquant à toute entrée autre qu'une entrée d'une famille. La tâche désignée par un objet d'un type de tâche possède (ou possède) les entrées déclarées dans la spécification du type de tâche.
Dans le corps d'une tâche, chacune de ses entrées uniques ou familles d'entrées peut être nommée par le nom simple correspondant. Le nom d'une entrée d'une famille prend la forme d'un composant indexé, le nom simple de la famille étant suivi de l'index entre parenthèses ; le type de cet index doit être le même que celui de l'intervalle discret dans la déclaration de famille d'entrées correspondante. En dehors du corps d'une tâche, un nom d'entrée a la forme d'un composant sélectionné, dont le préfixe désigne l'objet tâche et dont le sélecteur est le nom simple de l'une de ses entrées uniques ou familles d'entrées. 5 Une entrée unique surcharge un sous-programme, un littéral d'énumération ou une autre entrée unique si elles ont le même identifiant. La surcharge n'est pas définie pour les familles d'entrées. Une entrée unique ou une entrée d'une famille d'entrées peut être renommée en tant que procédure.
Les modes de paramètres définis pour les paramètres de la partie formelle d'une déclaration d'entrée sont les mêmes que pour une déclaration de sous-programme et ont la même signification. La syntaxe d'une instruction d'appel d'entrée est similaire à celle d'une instruction d'appel de procédure, et les règles d'association de paramètres sont les mêmes que pour les appels de sous-programme.
Une instruction accept spécifie les actions à effectuer lors de l'appel d'une entrée nommée (il peut s'agir d'une entrée d'une famille). La partie formelle d'une instruction accept doit être conforme à la partie formelle donnée dans la déclaration de l'entrée unique ou de la famille d'entrées nommée par l'instruction accept. Si un nom simple apparaît à la fin d'une instruction accept, il doit répéter celui donné au début.
Une instruction accept pour une entrée d'une tâche donnée n'est autorisée que dans le corps de la tâche correspondante; à l'exclusion du corps de toute unité de programme étant elle-même interne au corps de la tâche; et à l'exclusion d'une autre instruction accept pour la même entrée unique ou une entrée de la même famille. (Une conséquence de cette règle est qu'une tâche ne peut exécuter des instructions accept que pour ses propres entrées.) Un corps de tâche peut contenir plusieurs instructions accept pour la même entrée. Pour l'élaboration d'une déclaration d'entrée, l'intervalle discret, le cas échéant, est évaluée et la partie formelle, le cas échéant, est ensuite élaborée comme pour une déclaration de sous-programme.
L'exécution d'une instruction accept commence par l'évaluation de l'index d'entrée (dans le cas d'une entrée d'une famille). L'exécution d'une instruction d'appel d'entrée commence par l'évaluation du nom d'entrée; elle est suivie de toutes les évaluations requises pour les paramètres réels de la même manière que pour un appel de sous-programme. L'exécution ultérieure d'une instruction accept et d'une instruction d'appel d'entrée correspondante est synchronisée.
Si une entrée donnée est appelée par une seule tâche, il existe deux possibilités :
- Si la tâche appelante émet une instruction d'appel d'entrée avant qu'une instruction accept correspondante ne soit atteinte par la tâche propriétaire de l'entrée, l'exécution de la tâche appelante est suspendue.
- Si une tâche atteint une instruction accept avant tout appel de cette entrée, l'exécution de la tâche est suspendue jusqu'à ce qu'un tel appel soit reçu.
Lorsqu'une entrée a été appelée et qu'une instruction accept correspondante a été atteinte, la séquence d'instructions, le cas échéant, de l'instruction accept est exécutée par la tâche appelée (tandis que la tâche appelante reste suspendue). Cette interaction est appelée rendez-vous. Par la suite, la tâche appelante et la tâche propriétaire de l'entrée continuent leur exécution en parallèle.
Si plusieurs tâches appellent la même entrée avant qu'une instruction accept correspondante ne soit atteinte, les appels sont mis en file d'attente; il existe une file d'attente associée à chaque entrée. Chaque exécution d'une instruction accept supprime un appel de la file d'attente. Les appels sont traités dans l'ordre d'arrivée.
Une tentative d'appel d'une entrée d'une tâche ayant terminé son exécution lève l'exception TASKING_ERROR au point d'appel, dans la tâche appelante; de même, cette exception est levée au point d'appel si la tâche appelée termine son exécution avant d'accepter l'appel. L'exception CONSTRAINT_ERROR est levée si l'index d'une entrée d'une famille n'est pas dans l'intervalle discret spécifié.
Exemples de déclarations d'entrée :
- entry READ(V:out ITEM);
- entry SEIZE;
- entry REQUEST(LEVEL)(D : ITEM); -- une famille d'entrées
Exemples d'appels d'entrée :
- CONTROL.RELEASE;
- PRODUCER_CONSUMER.WRITE(E);
- POOL(5).READ(NEXT_CHAR);
- CONTROLLER.REQUEST(LOW)(SOME_ITEM);
Exemples d'instructions d'acceptation :
Remarques : La partie formelle donnée dans une instruction accept n'est pas élaborée; elle sert uniquement à identifier l'entrée correspondante.
Une instruction accept peut appeler des sous-programmes émettant des appels d'entrée. Une instruction accept n'a pas besoin d'avoir une séquence d'instructions même si l'entrée correspondante a des paramètres. De même, elle peut avoir une séquence d'instructions même si l'entrée correspondante n'a pas de paramètres. La séquence d'instructions d'une instruction accept peut inclure des instructions return. Une tâche peut appeler ses propres entrées mais elle se bloquera bien sûr. Le langage autorise les appels d'entrée conditionnels et temporisés. Les règles du langage garantissent qu'une tâche ne peut se trouver que dans une file d'attente d'entrées à un moment donné.
Si les limites de l'intervalle discret d'une famille d'entrées sont des littéraux entiers, l'index (dans un nom d'entrée ou une instruction accept) doit être du type prédéfini INTEGER.
Instructions Delay, Duration et Time
L'exécution d'une instruction de retard évalue l'expression simple et suspend l'exécution ultérieure de la tâche exécutant l'instruction de retard, pendant au moins la durée spécifiée par la valeur résultante.
instruction_delay ::= delay expression_simple |
L'expression simple doit être du type à virgule fixe prédéfini DURATION; sa valeur est exprimée en secondes ; une instruction delay avec une valeur négative est équivalente à une instruction delay avec une valeur nulle.
Toute implémentation du type DURATION doit permettre la représentation de durées (positives et négatives) jusqu'à au moins 86 400 secondes (un jour); la plus petite durée représentable, DURATION'SMALL, ne doit pas être supérieure à vingt millisecondes (dans la mesure du possible, une valeur ne dépassant pas cinquante microsecondes doit être choisie). Notez que DURATION'SMALL ne doit pas nécessairement correspondre au cycle d'horloge de base, le nombre nommé SYSTEM.TICK.
La définition du type TIME est fournie dans le paquet de bibliothèque prédéfini CALENDAR. La fonction CLOCK renvoie la valeur actuelle de TIME au moment où elle est appelée. Les fonctions YEAR, MONTH, DAY et SECONDS renvoient les valeurs correspondantes pour une valeur donnée du type TIME; la procédure SPLIT renvoie les quatre valeurs correspondantes. Inversement, la fonction TIME_OF combine un numéro d'année, un numéro de mois, un numéro de jour et une durée, en une valeur de type TIME. Les opérateurs "+" et "-" pour l'addition et la soustraction de temps et de durées, et les opérateurs relationnels pour les temps, ont la signification conventionnelle.
L'exception TIME_ERROR est levée par la fonction TIME_OF si les paramètres réels ne forment pas une date correcte. Cette exception est également levée par les opérateurs "+" et "-" si, pour les opérandes donnés, ces opérateurs ne peuvent pas renvoyer une date dont le numéro d'année est dans l'intervalle du sous-type correspondant, ou si l'opérateur ne peut pas renvoyer un résultat qui est dans l'intervalle du type DURATION.
- package CALENDAR is
- type TIME is private;
-
- subtype YEAR_NUMBER is INTEGER range 1901 .. 2099;
- subtype MONTH_NUMBER is INTEGER range 1 .. 12;
- subtype DAY_NUMBER is INTEGER range 1 .. 31;
- subtype DAY_DURATION is DURATION range 0.0 .. 86_400.0
-
- function CLOCK return TIME;
-
- function YEAR (DATE : TIME) return YEAR_NUMBER;
- function MONTH (DATE : TIME) return MONTH_NUMBER;
- function DAY (DATE : TIME) return DAY_NUMBER;
- function SECONDS (DATE : TIME) return DAY_DURATION;
-
- procedure SPLIT ( DATE : in TIME;
- YEAR : out YEAR_NUMBER;
- MONTH : out MONTH_NUMBER;
- DAY : out DAY_NUMBER;
- SECONDS : out DAY_DURATION);
-
- function TIME_OF( YEAR : YEAR_NUMBER;
- MONTH : MONTH_NUMBER;
- DAY : DAY_NUMBER;
- SECONDS : DAY_DURATION := 0.0) return TIME;
-
- function "+" (LEFT : TIME; RIGHT : DURATION) return TIME;
- function "+" (LEFT : DURATION; RIGHT : TIME) return TIME;
- function "-" (LEFT : TIME; RIGHT : DURATION) return TIME;
- function "-" (LEFT : TIME; RIGHT : TIME) return DURATION;
-
- function "<" (LEFT, RIGHT : TIME) return BOOLEAN;
- function "<=" (LEFT, RIGHT : TIME) return BOOLEAN;
- function ">" (LEFT, RIGHT : TIME) return BOOLEAN;
- function ">=" (LEFT, RIGHT : TIME) return BOOLEAN;
- TIME_ERROR : exception; -- peut être déclenché par TIME_OF, "+" et "-"
-
- private
- -- dépendant de l'implémentation
- end;
Exemples :
Remarques : Le deuxième exemple provoque la répétition de la boucle toutes les INTERVAL secondes en moyenne. Cet intervalle entre deux itérations successives n'est qu'approximatif. Cependant, il n'y aura pas de dérive cumulative tant que la durée de chaque itération est (suffisamment) inférieure à INTERVAL.
Les instructions de sélection
Il existe trois formes d'instructions de sélection. L'une d'entre elles permet une attente sélective pour une ou plusieurs alternatives. Les deux autres permettent des appels d'entrée conditionnels et temporisés :
instruction_de_selection ::= attente_sélective | appel_d_entree_conditionnel | appel_d_entree_chronometre |
Attentes sélectives
Cette forme d'instruction select permet une combinaison d'attente et de sélection d'une ou plusieurs alternatives. La sélection peut dépendre des conditions associées à chaque alternative de l'attente sélective :
attente_selective ::= select selectionner_alternative | or selectionner_alternative] | else sequence_d_instructions] end select; selectionner_alternative ::= [ when condition =>] alternative_d_attente_sélective alternative_d_attente_sélective ::= accepter_alternative | delai_alternatif | terminer_alternative accepter_alternative ::= declaration_d_acceptation [sequence_d_instructions] delai_alternatif ::= instruction_delay [sequence_d_instructions] terminer_alternative ::= terminate; |
Une attente sélective doit contenir au moins une alternative d'acceptation. De plus, une attente sélective peut contenir soit une alternative de terminaison (une seule), soit une ou plusieurs alternatives de retard, soit une partie else ; ces trois possibilités s'excluent mutuellement.
Une alternative de sélection est dite ouverte si elle ne commence pas par when et une condition, ou si la condition est TRUE. Elle est dite fermée dans le cas contraire.
Pour l'exécution d'une attente sélective, toutes les conditions spécifiées après when sont évaluées dans un ordre n'étant pas défini par le langage; les alternatives ouvertes sont ainsi déterminées. Pour une alternative de retard ouverte, l'expression de retard est également évaluée. De même, pour une alternative d'acceptation ouverte pour une entrée d'une famille, l'index d'entrée est également évalué. La sélection et l'exécution d'une alternative ouverte, ou de la partie else, achèvent alors l'exécution de l'attente sélective; les règles de cette sélection sont décrites ci-dessous.
Les alternatives d'acceptation ouvertes sont d'abord envisagées. La sélection d'une telle alternative a lieu immédiatement si un rendez-vous correspondant est possible, c'est-à-dire s'il existe un appel d'entrée correspondant émis par une autre tâche et en attente d'acceptation. Si plusieurs alternatives peuvent ainsi être sélectionnées, l'une d'elles est choisie arbitrairement (c'est-à-dire que le langage ne définit pas laquelle). Lorsqu'une telle alternative est sélectionnée, l'instruction accept correspondante et les éventuelles instructions subséquentes sont exécutées. Si aucun rendez-vous n'est immédiatement possible et qu'il n'y a pas de partie else, la tâche attend qu'une alternative ouverte d'attente sélective puisse être sélectionnée.
La sélection des autres formes d'alternative ou d'une partie else s'effectue de la manière suivante :
- Une alternative ouverte de délai sera sélectionnée si aucune alternative accept ne peut être sélectionnée avant que le délai spécifié ne soit écoulé (immédiatement, pour un délai négatif ou nul en l'absence d'appels d'entrée en file d'attente); les éventuelles instructions subséquentes de l'alternative sont alors exécutées. Si plusieurs alternatives delay peuvent ainsi être sélectionnées (c'est-à-dire si elles ont le même délai), l'une d'elles est choisie arbitrairement.
- La partie else est sélectionnée et ses instructions sont exécutées si aucune alternative accept ne peut être immédiatement sélectionnée, en particulier si toutes les alternatives sont fermées.
- Une alternative ouverte terminate est sélectionnée si les conditions énoncées sont satisfaites. C'est une conséquence d'autres règles qu'une alternative de terminaison ne peut pas être sélectionnée lorsqu'il existe un appel d'entrée en file d'attente pour une entrée de la tâche.
L'exception PROGRAM_ERROR est levée si toutes les alternatives sont fermées et qu'il n'y a pas de partie else.
Exemples d'une instruction select :
- select
- accept DRIVER_AWAKE_SIGNAL;
- or
- delay 30.0*SECONDS;
- STOP_THE_TRAIN;
- end select;
Exemple de corps de tâche avec une instruction select :
Remarques : Une attente sélective peut avoir plusieurs alternatives de délai ouvertes. Une attente sélective peut avoir plusieurs alternatives d'acceptation ouvertes pour la même entrée.
Appels d'entrée conditionnels
Un appel d'entrée conditionnel émet un appel d'entrée étant ensuite annulé si un rendez-vous n'est pas immédiatement possible :
appel_d_entree_conditionnel ::= select instruction_d_appel_d_entree [ sequence_d_instructions] else sequence_d_instructions end select |
Pour l'exécution d'un appel d'entrée conditionnel, le nom de l'entrée est d'abord évalué. Ceci est suivi par les évaluations requises pour les paramètres réels comme dans le cas d'un appel de sous-programme.
L'appel d'entrée est annulé si l'exécution de la tâche appelée n'a pas atteint un point où elle est prête à accepter l'appel (c'est-à-dire soit une instruction accept pour l'entrée correspondante, soit une instruction select avec une alternative accept ouverte pour l'entrée), ou s'il existe des appels d'entrée en file d'attente antérieurs pour cette entrée. Si la tâche appelée a atteint une instruction select, l'appel d'entrée est annulé si une alternative accept pour cette entrée n'est pas sélectionnée.
Si l'appel d'entrée est annulé, les instructions de la partie else sont exécutées. Sinon, le rendez-vous a lieu; et la séquence facultative d'instructions après l'appel d'entrée est alors exécutée.
L'exécution d'un appel d'entrée conditionnel déclenche l'exception TASKING_ERROR si la tâche appelée a déjà terminé son exécution.
Exemple :
Appels d'entrée temporisés
Un appel d'entrée temporisé émet un appel d'entrée étant annulé si un rendez-vous n'est pas démarré dans un délai donné :
appel_d_entrée_chronometre ::= select instruction_d_appel_d_entree [ sequence_d_instructions] or delai_alternatif end select; |
Pour l'exécution d'un appel d'entrée temporisé, le nom de l'entrée est d'abord évalué. Ceci est suivi par les évaluations requises pour les paramètres réels comme dans le cas d'un appel de sous-programme. L'expression indiquant le délai est ensuite évaluée et l'appel d'entrée est finalement émis.
Si un rendez-vous peut être démarré dans le délai spécifié (ou immédiatement, comme pour un appel d'entrée conditionnel, pour un délai négatif ou nul), il est exécuté et la séquence optionnelle d'instructions après l'appel d'entrée est alors exécutée. Sinon, l'appel d'entrée est annulé lorsque la durée spécifiée a expiré et la séquence optionnelle d'instructions de l'alternative de délai est exécutée.
L'exécution d'un appel d'entrée temporisé déclenche l'exception TASKING_ERROR si la tâche appelée termine son exécution avant d'accepter l'appel.
Exemple :
- select
- CONTROLLER.REQUEST(MEDIUM)(SOME_ITEM);
- or
- delay 45.0;
- -- le contrôleur est trop occupé, essayez autre chose
- end select;
Les priorités
Chaque tâche peut (mais n'est pas obligée) avoir une priorité, étant une valeur du sous-type PRIORITY (de type INTEGER) déclarée dans le paquet de bibliothèque prédéfini SYSTEM. Une valeur inférieure indique un degré d'urgence inférieur; l'intervalle de priorités est définie par l'implémentation. Une priorité est associée à une tâche si un pragma :
pragma PRIORITY (expression_statique); |
apparaît dans la spécification de tâche correspondante; la priorité est donnée par la valeur de l'expression. Une priorité est associée au programme principal si un tel pragma apparaît dans sa partie déclarative la plus externe. Au plus un pragma de ce type peut apparaître dans une spécification de tâche donnée ou pour un sous-programme étant une unité de bibliothèque, et ce sont les seuls emplacements autorisés pour ce pragma. Un pragma PRIORITY n'a aucun effet s'il apparaît dans un sous-programme autre que le programme principal.
La spécification d'une priorité est une indication donnée pour aider l'implémentation dans l'allocation de ressources de traitement à des tâches parallèles lorsqu'il y a plus de tâches éligibles à l'exécution que ce qui peut être pris en charge simultanément par les ressources de traitement disponibles. L'effet des priorités sur la planification est défini par la règle suivante :
- Si deux tâches avec des priorités différentes sont toutes deux éligibles à l'exécution et pourraient raisonnablement être exécutées en utilisant les mêmes processeurs physiques et les mêmes autres ressources de traitement, alors il ne peut pas se produire que la tâche avec la priorité la plus basse soit en cours d'exécution alors que la tâche avec la priorité la plus élevée ne l'est pas.
Pour les tâches de même priorité, l'ordre d'ordonnancement n'est pas défini par le langage. Pour les tâches sans priorité explicite, les règles d'ordonnancement ne sont pas définies, sauf lorsque ces tâches sont engagées dans un rendez-vous. Si les priorités des deux tâches engagées dans un rendez-vous sont définies, le rendez-vous est exécuté avec la plus élevée des deux priorités. Si une seule des deux priorités est définie, le rendez-vous est exécuté avec au moins cette priorité. Si aucune n'est définie, la priorité du rendez-vous est indéfinie.
Remarques : La priorité d'une tâche est statique et donc fixe. Cependant, la priorité lors d'un rendez-vous n'est pas nécessairement statique puisqu'elle dépend aussi de la priorité de la tâche appelant l'entrée. Les priorités ne doivent être utilisées que pour indiquer des degrés relatifs d'urgence; elles ne doivent pas être utilisées pour la synchronisation des tâches.
Attributs de tâche et d'entrée
Pour un objet de tâche ou une valeur T, les attributs suivants sont définis :
Attribut | Description |
---|---|
T'CALLABLE | Renvoie la valeur FALSE lorsque l'exécution de la tâche désignée par T est complétée ou terminée, ou lorsque la tâche est anormale. Renvoie la valeur TRUE dans le cas contraire. La valeur de cet attribut est du type prédéfini BOOLEAN. |
T'TERMINATED | Renvoie la valeur TRUE si la tâche désignée par T est terminée. Renvoie la valeur FALSE dans le cas contraire. La valeur de cet attribut est de type prédéfini BOOLEAN. |
De plus, les attributs de représentation STORAGE_SIZE, SIZE et ADDRESS sont définis pour un objet tâche T ou un type de tâche T.
L'attribut COUNT est défini pour une entrée E d'une unité de tâche T. L'entrée peut être soit une entrée unique, soit une entrée d'une famille (dans les deux cas, le nom de l'entrée unique ou de la famille d'entrées peut être un nom simple ou développé). Cet attribut n'est autorisé que dans le corps de T, mais exclut toute unité de programme étant elle-même interne au corps de T :
Attribut | Description |
---|---|
E'COUNT | Renvoie le nombre d'appels d'entrée actuellement en file d'attente sur l'entrée E (si l'attribut est évalué par l'exécution d'une instruction accept pour l'entrée E, le décompte n'inclut pas la tâche appelante). La valeur de cet attribut est de type universal_integer. |
Remarque : les algorithmes interrogeant l'attribut E'COUNT doivent prendre des précautions pour permettre l'augmentation de la valeur de cet attribut pour les appels d'entrée entrants, et sa diminution, par exemple avec les appels d'entrée temporisés.
L'instruction abort
Une instruction abort provoque le dérèglement d'une ou plusieurs tâches, empêchant ainsi tout rendez-vous ultérieur avec ces tâches :
instruction_abort ::= abort nom_de_tache [, nom_de_tache]; |
La détermination du type de chaque nom de tâche utilise le fait que le type du nom est un type de tâche.
Pour l'exécution d'une instruction abort, les noms de tâches donnés sont évalués dans un ordre qui n'est pas défini par le langage. Chaque tâche nommée devient alors anormale à moins qu'elle ne soit déjà terminée; de même, toute tâche dépendant d'une tâche nommée devient anormale à moins qu'elle ne soit déjà terminée.
Toute tâche anormale dont l'exécution est suspendue à une instruction accept, select ou delay devient terminée; toute tâche anormale dont l'exécution est suspendue à un appel d'entrée, et qui n'est pas encore dans un rendez-vous correspondant, devient terminée et est supprimée de la file d'attente d'entrée; toute tâche anormale qui n'a pas encore commencé son activation devient terminée (et donc également terminée). Cela termine l'exécution de l'instruction abort.
L'achèvement de toute autre tâche anormale n'a pas besoin de se produire avant l'achèvement de l'instruction abort. Il doit se produire au plus tard lorsque la tâche anormale atteint un point de synchronisation étant l'un des suivants : la fin de son activation ; un point où elle provoque l'activation d'une autre tâche; un appel d'entrée; le début ou la fin d'une instruction accept; d'une instruction select; d'une instruction delay; d'un gestionnaire d'exceptions; ou d'une instruction abort. Si une tâche appelant une entrée devient anormale alors qu'elle est dans un rendez-vous, sa terminaison n'a pas lieu avant la fin du rendez-vous.
L'appel d'une entrée d'une tâche anormale déclenche l'exception TASKING_ERROR à l'endroit de l'appel. De même, l'exception TASKING_ERROR est déclenchée pour toute tâche ayant appelé une entrée d'une tâche anormale, si l'appel d'entrée est toujours en file d'attente ou si le rendez-vous n'est pas encore terminé (que l'appel d'entrée soit une instruction d'appel d'entrée, ou un appel d'entrée conditionnel ou temporisé); l'exception est déclenchée au plus tard à la fin de la tâche anormale. La valeur de l'attribut CALLABLE est FALSE pour toute tâche anormale (ou terminée).
Si la fin anormale d'une tâche a lieu alors que la tâche met à jour une variable, alors la valeur de cette variable est indéfinie.
Exemple :
- abort USER, TERMINAL.all, POOL(3);
Remarques : une instruction abort ne doit être utilisée que dans des situations extrêmement graves nécessitant un arrêt inconditionnel. Une tâche est autorisée à abandonner n'importe quelle tâche, y compris elle-même.
Variables partagées
Les moyens habituels de communication de valeurs entre tâches sont les appels d'entrée et les instructions d'acceptation.
Si deux tâches lisent ou mettent à jour une variable partagée (c'est-à-dire une variable accessible par les deux), aucune d'elles ne peut alors supposer quoi que ce soit sur l'ordre dans lequel l'autre effectue ses opérations, sauf aux points où elles se synchronisent. Deux tâches sont synchronisées au début et à la fin de leur rendez-vous. Au début et à la fin de son activation, une tâche est synchronisée avec la tâche provoquant cette activation. Une tâche ayant terminé son exécution est synchronisée avec toute autre tâche.
Pour les actions effectuées par un programme utilisant des variables partagées, les hypothèses suivantes peuvent toujours être faites :
- Si entre deux points de synchronisation d'une tâche, cette tâche lit une variable partagée dont le type est un scalaire ou un type d'accès, alors la variable n'est mise à jour par aucune autre tâche à aucun moment entre ces deux points.
- Si entre deux points de synchronisation d'une tâche, cette tâche met à jour une variable partagée dont le type est un type scalaire ou d'accès, alors la variable n'est ni lue ni mise à jour par aucune autre tâche à aucun moment entre ces deux points.
L'exécution du programme est erronée si l'une de ces hypothèses est violée.
Si une tâche donnée lit la valeur d'une variable partagée, les hypothèses ci-dessus permettent à une implémentation de conserver des copies locales de la valeur (par exemple, dans des registres ou dans une autre forme de stockage temporaire); et tant que la tâche donnée n'atteint pas un point de synchronisation ni ne met à jour la valeur de la variable partagée, les hypothèses ci-dessus impliquent que, pour la tâche donnée, la lecture d'une copie locale équivaut à la lecture de la variable partagée elle-même.
De même, si une tâche donnée met à jour la valeur d'une variable partagée, les hypothèses ci-dessus permettent à une implémentation de conserver une copie locale de la valeur et de différer l'entreposage effectif de la copie locale dans la variable partagée jusqu'à un point de synchronisation, à condition que chaque lecture ou mise à jour supplémentaire de la variable par la tâche donnée soit traitée comme une lecture ou une mise à jour de la copie locale. D'autre part, une implémentation n'est pas autorisée à introduire un entreposage, à moins que ce stockage ne soit également exécuté dans l'ordre canonique.
Le pragma SHARED peut être utilisé pour spécifier que chaque lecture ou mise à jour d'une variable est un point de synchronisation pour cette variable ; c'est-à-dire que les hypothèses ci-dessus sont toujours valables pour la variable donnée (mais pas nécessairement pour les autres variables). La forme de ce pragma est la suivante :
pragma SHARED(variable_simple_name); |
Ce pragma n'est autorisé que pour une variable déclarée par une déclaration d'objet et dont le type est un type scalaire ou d'accès; la déclaration de variable et le pragma doivent tous deux apparaître (dans cet ordre) immédiatement dans la même partie déclarative ou spécification de paquet; le pragma doit apparaître avant toute occurrence du nom de la variable, autre que dans une clause d'adresse.
Une implémentation doit restreindre les objets pour lesquels le pragma SHARED est autorisé aux objets pour lesquels la lecture directe et la mise à jour directe sont implémentées comme une opération indivisible.
Exemple de tâche
L'exemple suivant définit une tâche de mise en mémoire tampon pour lisser les variations entre la vitesse de sortie d'une tâche de production et la vitesse d'entrée d'une tâche de consommation. Par exemple, la tâche de production peut contenir les instructions suivantes :
- loop
- -- produire le caractère suivant CHAR
- BUFFER.WRITE(CHAR);
- exit when CHAR = ASCII.EOT;
- end loop;
et la tâche consommatrice peut contenir les instructions :
- loop
- BUFFER.READ(CHAR);
- -- consommer le caractère CHAR
- exit when CHAR = ASCII.EOT;
- end loop;
La tâche de mise en mémoire tampon contient un bassin interne de caractères traités de manière circulaire. Le pool possède deux index, un IN_INDEX indiquant l'espace pour le prochain caractère d'entrée et un OUT_INDEX indiquant l'espace pour le prochain caractère de sortie :
- task BUFFER is
- entry READ (C : out CHARACTER);
- entry WRITE (C : in CHARACTER);
- end;
-
- task body BUFFER is
- POOL_SIZE : constant INTEGER := 100;
- POOL : array( 1 .. POOL_SIZE) of CHARACTER;
- COUNT : INTEGER range 0 .. POOL_SIZE := 0;
- IN_INDEX, OUT_INDEX : INTEGER range 1 .. POOL_SIZE := 1;
- begin
- loop
- select
- when COUNT < POOL_SIZE =>
- accept WRITE(C : in CHARACTER) do
- POOL(IN_INDEX) := C;
- end;
- IN_INDEX := IN_INDEX mod POOL_SIZE + 1;
- COUNT := COUNT + 1;
- or when COUNT > 0 =>
- accept READ(C : out CHARACTER) do
- C := POOL(OUT_INDEX);
- end;
- OUT_INDEX := OUT_INDEX mod POOL_SIZE + 1;
- COUNT := COUNT - 1;
- or
- terminate;
- end select;
- end loop;
- end BUFFER;