• Aucun résultat trouvé

Chapitre 3 : Programmes Ada Concurrents

2. Concepts de Base

Un programme Ada est composé d’une ou plusieurs unités de programmes. Les unités de programmes peuvent être des sous-programmes (définissant des algorithmes exécutables), des packages (définissant des collections d’entités), des unités de tâche (définissant des calculs concurrents), des unités protégées (définissant des opérations pour un partage coordonné entre tâches), ou des unités génériques (définissant des formes paramétrées des packages ou sous-programmes). Normalement, chaque unité de programme contient deux parties : une spécification contenant l’information qui peut être visible par d’autres unités, un corps contenant les détails d’implémentation, qui n’a pas besoin d’être visible par d’autres unités. La plupart des unités de programme peuvent être compilées séparément. Cette distinction entre la spécification et le corps, et la capacité de compiler les unités séparément, permet à un programme d’être conçu, écrit et testé comme un ensemble des composants larges indépendants d’un logiciel. Normalement, un programme Ada peut être mis dans une librairie pour une utilité générale. Le langage fournit des moyens par lequel des organisations individuelles peuvent construire leurs propres librairies. Toutes les librairies sont structurées dans une manière hiérarchique. Ceci permet une décomposition logique d’un sous-système à des composants individuels. Le texte d'une unité de programme compilée séparément doit nommer les unités de librairie dont il a besoin.

2.1. Unités de Programme

Un sous-programme est l'unité de base pour exprimer un algorithme. Le langage Ada supporte les deux types de sous-programmes : procédures et fonctions. Un package est l'unité de base pour définir une collection d'entités reliées logiquement. Par exemple, un package peut être utilisé pour définir un ensemble de déclarations de type et d’opérations associées. Des parties d'un package peuvent être cachées de l'utilisateur, de ce fait permettant l'accès seulement aux propriétés logiques exprimées par les spécifications de package. Des unités de sous-programme et de package peuvent être compilées séparément et arrangées dans les hiérarchies des unités de parent et d'enfant donnant un bon contrôle de visibilité des propriétés logiques et de leur exécution détaillée. Une unité de tâche est l'unité de base pour définir une tâche dont l'ordre des actions peut être exécuté en concurrence avec celles d’autres tâches. De Telles tâches peuvent être mises en application sur des multi computers, multiprocesseurs, ou avec l'exécution intercalée (interleaved) sur un processeur simple. Une unité de tâche peut définir soit une exécution simple d’une tâche ou un ‘type de tâche’ permettant la création de plusieurs tâches semblables. Une unité protégée est l'unité de base pour définir des opérations protégées pour l'usage coordonné des données partagées entre les tâches. L'exclusion mutuelle simple est fournie automatiquement, et des protocoles de partage plus raffinés peuvent être définis. Une opération protégée peut être un sous-programme ou une entrée. Une entrée protégée indique une expression booléenne (une barrière d'entrée) qui doit être vraie

avant que le corps de l'entrée soit exécuté. Une unité protégée peut définir un objet protégé simple ou un type protégé permettant la création de plusieurs objets semblables.

2.2. Déclarations et Instructions

Le corps d’une unité de programme contient généralement deux parties : une partie déclarative et une séquence d’instructions. La première partie définit les entités logiques au sein de l’unité du programme. La seconde partie définit l’exécution de l’unité du programme.

La partie déclarative associe des noms à des entités déclarées. Par exemple, un nom peut dénoter un type, une constante, une variable ou une exception. Cette partie introduit les noms et les paramètres des autres sous-programmes imbriqués, packages, unités de tâche, unités protégées et des unités génériques pour être utilisés dans l’unité de programme.

La séquence des instructions décrit une séquence des actions qui peuvent être exécutées. Les instructions sont exécutées en succession (à moins qu’un transfert de contrôle provoque l’exécution de continuer d’un autre endroit). Une instruction d’affection change la valeur d’une variable. Un appel d’une procédure invoque l’exécution d’une procédure après l’association des paramètres actuels fournis par l’appel aux paramètres formels correspondants.

Les instructions case et if permettent une sélection basée sur la valeur d’une expression ou celle d’une condition, d’une séquence d’instructions appropriée. L’instruction loop fournit le mécanisme itératif de base du langage. Une instruction loop spécifie qu’une séquence des instructions s’exécute d’une façon répétitive dirigée selon un arrangement itératif, ou jusqu’à une instruction exit est déclenchée. Une instruction block comprend une séquence des instructions précède par des entités locales utilisées par les instructions.

Certaines instructions sont associées à des exécutions concurrentes. Une instruction delay retarde l'exécution d'une tâche pour une durée indiquée ou jusqu'à un temps indiqué. L’instruction d’appel d’une entrée entry call est similaire à une instruction d’appel de procédure. Il demande une opération sur une tâche ou sur un objet protégé, bloquant l’appelant jusqu'à ce que l'opération puisse être exécutée. Une tâche appelée peut accepter un appel d'entrée en exécutant l’instruction correspondante accept qui spécifie les actions qui vont être exécutées. Ces actions font partie du rendez-vous avec la tâche appelante. Un appel d’entrée sur un objet protégé est traité quand la barrière d'entrée correspondante est évaluée vraie, sur quoi le corps de l'entrée est exécuté. L’instruction ‘requeue’ permet la disposition d'un service comme un nombre d'activités liées avec le contrôle privilégié. Une forme de l’instruction select permet un sélectif attendant d’un ou de plusieurs rendez-vous possibles. D'autres formes de l’instruction select permettent des appels d'entrée conditionnels ou temporisés et le transfert asynchrone de contrôle en réponse à un certain événement de déclenchement.

L'exécution d'une unité de programme peut rencontrer des situations d'erreur dans lesquelles l'exécution de programme normale ne peut pas continuer. Par exemple, un calcul arithmétique peut excéder la valeur maximale permise pour un nombre, ou une tentative peut être faite pour avoir accès à un composant de tableau en utilisant

une valeur d'index incorrecte. Pour traiter de telles situations d'erreur, les instructions d'une unité de programme peuvent être textuellement suivies par les gestionnaires d'exception qui spécifient les actions à être prises quand la situation d'erreur surgit. Les exceptions peuvent être soulevées explicitement par une instruction raise. 2.3. Types de Données

Chaque objet dans le langage a un type, qui caractérise un ensemble de valeurs et un ensemble d'opérations applicables. Les classes principales des types sont des types élémentaires (incluant l'énumération, le type numérique et les types access) et des types composés (incluant les tableaux et les enregistrements). Un type d'énumération définit un ensemble ordonné de littéraux distinct d'énumération, comme une liste d'états ou un alphabet de caractères. Les types d'énumération Boolean, Character, et Wide_Character sont prédéfinis. Les types numériques fournissent le moyen d'exécuter exactement ou de rapprochement des calculs numériques. Les calculs exacts utilisent des types d'entier qui dénotent les ensembles d'entiers consécutifs. Des calculs approximatifs utilisent soit des types de point fixes, avec des limites absolues sur l'erreur, ou des types de virgule flottante, avec des limites relatives sur l'erreur. Les types numériques Integer, Float, et Duration sont prédéfinis.

Les types composés permettent des définitions des objets structurés avec les composants relatifs. Les types composés dans le langage Ada sont les tableaux et les enregistrements. Un tableau (array) est un objet avec des composants classés du même type. Un enregistrement (record) est un objet avec des composants de types probablement différents. Les tâches et les types protégés sont également des formes de types composés. Les types tableaux de String et Wide_String sont prédéfinis. Les types d’enregistrement, de tâche et les types protégés peuvent avoir des composants spéciaux appelés les discriminants qui paramétrisent le type. Des différentes structures record qui dépendent des valeurs des discriminants, peuvent être définies dans un type

record. Les types access permettent la construction des structures de données liées. Une valeur d'un type access représente une référence à un objet déclaré comme aliased ou à un objet créé par l'évaluation d'un allocateur. Plusieurs variables d'un type access peuvent indiquer le même objet, et les composants d'un objet peuvent indiquer le même ou d'autres objets. Les deux éléments dans de telles structures de données liées et leur relation avec d'autres éléments peuvent être changés pendant l'exécution du programme. Les types access permettent également à des références aux sous-programmes d’être stockées, d’être passées comme paramètres, et finalement d’être déréférenciés en tant qu’une partie d'un appel indirect. Les types privés permettent des vues limitées d'un type. Un type privé peut être défini dans un package de sorte que seulement les propriétés logiquement nécessaires soient rendues visibles aux utilisateurs du type. Tous les détails structuraux qui sont extérieurement non pertinents sont alors seulement disponibles dans le package et toutes les unités enfant. De n'importe quel type, un nouveau type peut être défini par dérivation. Un type, ainsi que ses dérivés (les deux direct et indirect) forment une classe de dérivation. On peut définir des larges classes d’opérations qui acceptent comme paramètre un opérande de tout type dans une classe de dérivation. Pour les types record et privés, les dérivés peuvent être des extensions du type parent. Les types qui soutiennent ces possibilités orienté-objet des larges classes d’opérations et l’extension du type doivent être étiquetés, de sorte que le type spécifique d'un opérande dans une classe de dérivation puisse être identifié au temps d'exécution. Quand une opération d'un type étiqueté est appliquée à un opérande dont le type spécifique n'est pas connu jusqu'au temps d'exécution,

encore raffiné par le concept d'un sous-type, par lequel un utilisateur puisse contraindre l'ensemble de valeurs permises d'un type. Des sous-types peuvent être employés pour définir des sous-ranges (subranges) des types scalaires, des tableaux avec un ensemble limité des valeurs d'index, des types record et des types privés avec des valeurs discriminantes particulières.

2.4. Objets Actifs

Les objets passifs agissent par l’intermédiaire des sous-programmes appelés (finalement) par le programme principal. Ils ne font rien de leur propre entente, seulement si on leurs demande de faire ainsi. Cependant, les objets actifs sont souvent bien utiles aussi. Ada nous permet de définir les tâches qui sont exécutées indépendamment. Le programme lui-même est exécuté par un environnement des tâches, et ceci est permis de créer d’autres tâches qui sont exécutées parallèlement avec d’autres tâches dans le programme. Notons que ce dispositif d’Ada dépend fortement du système d’exploitation ; un système d'exploitation comme le MS-DOS n'a aucune possibilité de traitement de sorte que les implémentations d’Ada pour des systèmes de MS-DOS puissent seulement fournir des possibilités faibles de traitement multitâche, le cas échéant. Heureusement la situation a changé, et les systèmes d'exploitation comme Unix, OS/2 et Windows/NT permettent les traitements multitâches.