Concurrence sécurisée avec Ada ── Guide pratique des tâches et objets protégés
· Mis à jour le: · Go Komura · Ada, Concurrence, Tâches, Objets Protégés, Rendez-vous, Temps Réel, Programmation Parallèle, Langage de Programmation, Haute Fiabilité
1. Introduction ── La concurrence intégrée au langage
La concurrence est un sujet incontournable du développement logiciel moderne. Pourtant, dans la plupart des langages, elle reste une fonctionnalité « ajoutée après coup », dépendante de bibliothèques ou de mécanismes du système d’exploitation, qui exige des connaissances approfondies et une conception rigoureuse pour être utilisée correctement.
Ada apporte une réponse singulière à ce problème : la concurrence est intégrée à la spécification même du langage.
Modèle de concurrence d'Ada :
- Tâche (task) ── unité de concurrence exécutée indépendamment
- Rendez-vous (rendezvous) ── communication synchrone entre tâches
- Objet protégé (protected object) ── exclusion mutuelle gérée par le langage
- Priorités temps réel ── fonctionnalités temps réel de l'Annexe D
Les tâches et le rendez-vous existent depuis Ada 83, en 1983 ; les objets protégés et les fonctionnalités temps réel de l’Annexe D ont été ajoutés avec Ada 95, avant de continuer à évoluer avec Ada 2005 puis Ada 2012. Plutôt que des primitives de synchronisation de bas niveau comme les mutex ou les sémaphores, la principale caractéristique de la concurrence en Ada est de permettre d’exprimer directement l’intention de conception dans le code.
Cet article présente la concurrence en Ada de façon progressive, à travers huit exemples de code pratiques. Chaque exemple est un extrait indépendant, réellement compilable et exécutable, que vous pouvez essayer vous-même.
Les fragments de code présentés dans cet article sont par ailleurs publiés sur GitHub sous forme d’une collection de référence, organisée par fichier pour chaque section.
ada-task-concurrency - komurasoft-blog-samples (GitHub)
2. Rappel rapide des dangers de la concurrence
Avant d’entrer dans le vif du sujet avec Ada, rappelons brièvement pourquoi une concurrence « sécurisée » est si importante.
Voici les bogues classiques rencontrés en programmation concurrente :
- Concurrence d’accès (data race) : plusieurs threads accèdent simultanément au même emplacement mémoire, dont au moins un en écriture. Le résultat est indéterminé.
- Interblocage (deadlock) : plusieurs tâches attendent indéfiniment que les autres se terminent, sans qu’aucune ne puisse progresser.
- Inversion de priorité (priority inversion) : une tâche de haute priorité attend une ressource détenue par une tâche de basse priorité, tandis qu’une tâche de priorité intermédiaire préempte cette dernière.
- Famine (starvation) : une tâche ne parvient jamais à obtenir la ressource dont elle a besoin.
Le modèle de concurrence d’Ada apporte des parades au niveau du langage contre ces problèmes.
Concurrence d'accès → les objets protégés garantissent un accès exclusif
Interblocage → le modèle de rendez-vous fournit une synchronisation structurée
Inversion de priorité → le Priority Ceiling Protocol est disponible nativement dans le langage
Famine → contrôlée par les barrières d'entrée et les politiques de mise en file
3. Les bases des tâches ── unités d’exécution indépendantes
L’unité de base de la concurrence en Ada est la tâche (task). Une tâche ressemble à un thread, mais ne correspond pas nécessairement une à une à un thread du système d’exploitation : c’est le runtime Ada qui gère l’ordonnancement.
task Greeter is
entry Start;
end Greeter;
task body Greeter is
begin
accept Start;
Put_Line ("Hello from a task!");
end Greeter;
Ce code (01_hello_task.ada) illustre plusieurs points importants.
Une tâche commence à s’exécuter automatiquement dès sa déclaration. La tâche Greeter démarre au moment où la procédure qui la contient atteint begin, puis attend à accept Start; une demande de rendez-vous de la part de l’appelant.
Une entrée (entry) est l’interface qu’une tâche expose vers l’extérieur. Lorsque l’appelant invoque Greeter.Start;, il se synchronise avec accept Start; du côté de la tâche. C’est ce qu’on appelle un rendez-vous.
La fin des tâches est attendue automatiquement. Lorsque la procédure principale se termine, si des tâches sont encore en cours d’exécution, leur achèvement est attendu implicitement. Cela contraste avec les plantages causés, en C++, par l’oubli d’appeler std::thread::join.
4. Le rendez-vous ── une communication synchrone qui transmet des données
Le rendez-vous n’est pas qu’une simple synchronisation : il permet aussi un transfert de données bidirectionnel.
task Worker is
entry Compute (X, Y : Integer; Result : out Integer);
end Worker;
task body Worker is
A, B : Integer;
Output : Integer;
begin
accept Compute (X, Y : Integer; Result : out Integer) do
A := X;
B := Y;
Output := A * A + B * B;
Result := Output;
end Compute;
end Worker;
L’appelant l’utilise de la façon suivante (02_rendezvous_intro.ada).
Worker.Compute (3, 4, Answer);
Put_Line ("Main: result = " & Integer'Image (Answer));
Le point de conception essentiel ici est que les modes de paramètres sont explicites :
- mode
in: transmet une valeur de l’appelant vers la tâche - mode
out: renvoie un résultat de la tâche vers l’appelant - mode
in out: bidirectionnel
Le bloc do ... end à l’intérieur du corps de l’accept constitue la section critique. Pendant cette phase, l’appelant est bloqué et la tâche n’accepte aucune autre entrée. Une fois le traitement terminé, les deux reprennent leur exécution.
Pour résumer les caractéristiques du rendez-vous :
| Caractéristique | Description |
|---|---|
| Synchronisation | L’appelant et la tâche attendent tous deux d’atteindre en même temps le point de rendez-vous |
| Transfert de données | Les paramètres in / out / in out permettent de faire circuler des valeurs dans les deux sens |
| Exclusion mutuelle | Pendant l’exécution du corps de l’accept, aucune autre entrée de la tâche n’est acceptée |
| Structuration | Le corps de la tâche indique explicitement quelle entrée est acceptée, et quand |
5. Acceptation sélective ── attendre plusieurs services à la fois
Une tâche serveur réelle doit souvent attendre plusieurs types de requêtes différentes. L’instruction select d’Ada réalise cela au niveau du langage.
task Server is
entry Deposit (Amount : Integer);
entry Withdraw (Amount : Integer; Success : out Boolean);
entry Balance (Value : out Integer);
end Server;
task body Server is
Current : Integer := 0;
begin
loop
select
accept Deposit (Amount : Integer) do
Current := Current + Amount;
end Deposit;
or
accept Withdraw (Amount : Integer; Success : out Boolean) do
if Current >= Amount then
Current := Current - Amount;
Success := True;
else
Success := False;
end if;
end Withdraw;
or
accept Balance (Value : out Integer) do
Value := Current;
end Balance;
or
terminate;
end select;
end loop;
end Server;
Dans l’instruction select de ce code (03_selective_accept.ada), plusieurs branches or sont proposées, et l’une des entrées ayant un appel en attente est sélectionnée (le choix précis est défini par l’implémentation). Si aucune entrée n’a été appelée, la tâche attend jusqu’à ce que l’une d’elles le soit.
or terminate; est une branche spéciale qui permet à la tâche de se terminer proprement lorsque « la procédure principale s’est achevée et que plus personne ne peut appeler d’entrée sur cette tâche ». C’est un mécanisme propre à Ada qui résout le problème de la « tâche serveur qui attend indéfiniment », source classique d’interblocage.
La force de l’acceptation sélective est qu’elle permet aussi d’écrire des conditions de garde.
select
when Count > 0 =>
accept Get_Item (Item : out Integer) do
Item := Data (Head);
Count := Count - 1;
end Get_Item;
or
when Count < Max =>
accept Put_Item (Item : Integer) do
Data (Tail) := Item;
Count := Count + 1;
end Put_Item;
end select;
Une branche dont la condition de garde est fausse est exclue de la sélection à ce moment précis. Cela permet d’exprimer de façon déclarative un contrôle du type : « si le tampon est vide, faire attendre Get ; s’il est plein, faire attendre Put ».
6. Producteur-consommateur ── se synchroniser par rendez-vous
Voyons le schéma producteur-consommateur, un cas d’usage classique du rendez-vous.
task Consumer is
entry Deliver (Item : Integer);
end Consumer;
task Producer;
task body Consumer is
Sum : Integer := 0;
begin
for I in 1 .. 5 loop
accept Deliver (Item : Integer) do
Sum := Sum + Item;
end Deliver;
end loop;
end Consumer;
task body Producer is
begin
for I in 1 .. 5 loop
Consumer.Deliver (I);
end loop;
end Producer;
Dans ce schéma (04_producer_consumer.ada), chaque appel à Deliver par le producteur le synchronise avec le consommateur. Si le producteur est trop rapide, il attend que le consommateur exécute accept ; si le consommateur est trop rapide, il attend le prochain appel du producteur. Il en résulte une contre-pression (backpressure) naturelle.
7. Objets protégés ── une exclusion mutuelle sans verrou
Alors que la tâche est un « agent actif d’exécution », l’objet protégé (protected object) est le mécanisme dédié aux « données partagées passives ».
protected Counter is
procedure Increment;
function Value return Integer;
private
Count : Integer := 0;
end Counter;
protected body Counter is
procedure Increment is
begin
Count := Count + 1;
end Increment;
function Value return Integer is
begin
return Count;
end Value;
end Counter;
Les règles essentielles des objets protégés sont les suivantes :
- Une fonction (function) est en lecture seule. Plusieurs tâches peuvent l’appeler simultanément.
- Une procédure (procedure) est en lecture-écriture. Pendant son exécution, toutes les autres procédures et fonctions sont bloquées.
- Une entrée (entry) possède une barrière. L’appelant attend dans une file jusqu’à ce que la condition de barrière devienne vraie.
Dans ce code (05_protected_counter.ada), trois tâches ouvrières appellent chacune Increment 1 000 fois. Comme l’objet protégé garantit l’exclusion mutuelle, la valeur finale du compteur vaut toujours exactement 3 000. Il n’y a aucun verrouillage ni déverrouillage de mutex à écrire manuellement.
task type Worker (Id : Integer; Rounds : Integer);
task body Worker is
begin
for I in 1 .. Rounds loop
Counter.Increment; -- l'objet protégé garantit l'exclusion
end loop;
end Worker;
W1 : Worker (1, 1_000);
W2 : Worker (2, 1_000);
W3 : Worker (3, 1_000);
Que se passe-t-il sans objet protégé ?
Pour mesurer l’apport d’un objet protégé, examinons le code dangereux obtenu sans protection.
-- ⚠ Danger : manipulation directe d'une variable partagée
Shared_Counter : Integer := 0;
task body Bad_Worker is
begin
for I in 1 .. 10_000 loop
Shared_Counter := Shared_Counter + 1; -- concurrence d'accès !
end loop;
end Bad_Worker;
Au niveau du CPU, Shared_Counter := Shared_Counter + 1 se décompose en trois étapes : lecture, addition, réécriture. Lorsque plusieurs tâches exécutent cela simultanément, le résultat de l’addition d’une tâche peut ne pas tenir compte de la lecture d’une autre, et des incréments sont perdus. Pire encore, il s’agit d’une exécution erronée (erroneous execution) au sens de la Ada RM 9.10 : des lectures et écritures concurrentes sur une variable partagée non synchronisée ne se limitent pas à fausser la valeur finale du compteur, elles peuvent faire dériver le comportement de l’ensemble du programme vers un résultat arbitraire. Même avec deux tâches exécutant chacune 10 000 itérations, rien ne garantit que la valeur finale soit 20 000.
L’objet protégé prévient ce problème « au niveau de la syntaxe » : il suffit d’appeler Counter.Increment; pour que le compilateur et le runtime garantissent l’exclusion mutuelle.
8. Entrées protégées et barrières ── le tampon borné
Ajouter des entrées à un objet protégé permet une synchronisation conditionnelle. Examinons le cas classique du tampon borné (bounded buffer).
type Buffer_Array is array (0 .. Buffer_Size - 1) of Integer;
protected Buf is
entry Put (Item : Integer);
entry Get (Item : out Integer);
private
Data : Buffer_Array;
Head : Integer := 0;
Tail : Integer := 0;
Count : Integer := 0;
end Buf;
protected body Buf is
entry Put (Item : Integer) when Count < Buffer_Size is
begin
Data (Tail) := Item;
Tail := (Tail + 1) mod Buffer_Size;
Count := Count + 1;
end Put;
entry Get (Item : out Integer) when Count > 0 is
begin
Item := Data (Head);
Head := (Head + 1) mod Buffer_Size;
Count := Count - 1;
end Get;
end Buf;
when Count < Buffer_Size est la barrière (barrier). Elle est évaluée à chaque appel d’entrée : si elle est vraie, l’exécution se poursuit ; si elle est fausse, la tâche appelante attend dans une file. Chaque fois que l’état du tampon change (une autre tâche exécute Put ou Get), les barrières des tâches en attente sont réévaluées.
Ce schéma (06_bounded_buffer.ada) est l’un de ceux où les objets protégés d’Ada révèlent tout leur intérêt. Comparez-le à l’équivalent écrit en C avec un mutex et une variable de condition pthread :
// En C avec pthread (à comparer avec Ada)
pthread_mutex_lock(&mutex);
while (count >= BUFFER_SIZE) { // équivalent du « when » d'Ada
pthread_cond_wait(¬_full, &mutex); // attente sur la barrière
}
data[tail] = item;
tail = (tail + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(¬_empty); // notifie les tâches en attente
pthread_mutex_unlock(&mutex);
En Ada, tout cela se résume à la seule ligne when Count < Buffer_Size. La condition de la boucle while, l’envoi du signal, les erreurs de timing dans le relâchement du verrou — toutes ces sources de bogues disparaissent.
9. Appels avec délai d’expiration ── ne jamais attendre indéfiniment
Dans un système temps réel, « attendre indéfiniment » n’est pas acceptable. Ada prend en charge les délais d’expiration grâce à la construction select ... or delay.
select
Slow_Worker.Do_Work (Result);
Put_Line ("Main: work completed");
or
delay until Ada.Real_Time.Clock + Milliseconds (500);
Put_Line ("Main: timeout after 500ms!");
end select;
Dans ce code (07_timed_entry.ada), Slow_Worker est occupé dans un delay 2.0 et n’a pas encore atteint son accept : l’appel d’entrée mis en file expire donc au bout de 500 ms. (Le délai d’expiration s’applique au temps d’attente en file avant l’acceptation du rendez-vous ; il n’interrompt pas l’exécution du corps du rendez-vous lui-même.) delay until spécifie une heure absolue : c’est la technique de base de la programmation temps réel pour éviter toute dérive cumulative.
Ada prend également en charge l’appel d’entrée conditionnel (conditional entry call).
select
Server.Process (Item);
else
Put_Line ("Server is busy, will retry later");
end select;
La clause else permet de passer immédiatement à un traitement de repli si le rendez-vous ne peut pas avoir lieu tout de suite. Il n’est pas nécessaire d’écrire soi-même une boucle de scrutation (polling).
Ne pas oublier la conception de l’après-délai d’expiration
Les délais d’expiration sont pratiques, mais l’essentiel de la conception réside dans « que faire une fois l’attente écourtée ». Peut-on vraiment jeter la valeur ? Faut-il réessayer ? Faut-il remonter une erreur au niveau supérieur ? Laisser ces questions dans le flou se transforme, en production, en perte de données ou en interruption de service. Lorsque vous écrivez un délai d’expiration, concevez au même endroit la responsabilité de ce qui se passe après.
Tâches périodiques et delay until
delay until ne sert pas seulement aux délais d’expiration : il permet aussi l’exécution périodique. Avec un simple delay 0.1, la période devient « temps de traitement + 0,1 seconde », alors que delay until fixe le prochain point de départ à une heure absolue, ce qui garantit une période stable, indépendante de la durée du traitement.
loop
Next := Next + Period;
Do_Work;
delay until Next;
end loop;
Ce schéma est efficace partout où un traitement à intervalle fixe est requis : surveillance de capteurs, boucles de contrôle, et bien d’autres cas.
10. Priorités des tâches et ordonnancement temps réel
Les fonctionnalités temps réel d’Ada sont définies dans l’Annexe D (Real-Time Systems). Lorsqu’une implémentation Ada prend en charge l’Annexe D, il est possible de spécifier la priorité des tâches et la politique d’ordonnancement.
task High_Task is
pragma Priority (System.Default_Priority + 5);
end High_Task;
task Low_Task is
pragma Priority (System.Default_Priority);
end Low_Task;
Pour une configuration plus avancée, la politique d’ordonnancement et le protocole de plafond de priorité peuvent également être spécifiés :
pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
pragma Locking_Policy (Ceiling_Locking);
Le Priority Ceiling Protocol (protocole de plafond de priorité) est conçu pour empêcher l’inversion de priorité. Chaque objet protégé se voit attribuer explicitement une priorité plafond via pragma Priority (ou l’aspect Priority). Si la priorité active de la tâche appelante dépasse ce plafond, un Program_Error est levé. Pendant que l’objet est verrouillé, l’exécution se fait à la priorité plafond, ce qui empêche la préemption par des tâches de priorité intermédiaire.
protected Shared_Data is
pragma Priority (15); -- priorité plafond
procedure Update (Val : Integer);
function Read return Integer;
private
Data : Integer := 0;
end Shared_Data;
Ces fonctionnalités reposent sur les fondements théoriques du Rate Monotonic Scheduling (RMS) et ont fait leurs preuves dans des systèmes temps réel strict tels que les commandes de vol aéronautiques ou les dispositifs médicaux.
11. Principes de conception pour la pratique
Nous avons parcouru jusqu’ici la syntaxe de base des tâches et des objets protégés. Pour conclure, résumons les principes de conception à garder à l’esprit lorsque l’on utilise la concurrence d’Ada dans un contexte professionnel.
Ce qu’il ne faut pas faire dans un objet protégé
À l’intérieur d’un objet protégé, la règle d’or est de ne faire que de brèves mises à jour d’état et d’exécuter les traitements lourds à l’extérieur. Les opérations d’un objet protégé sont exclusives mutuellement en interne : bloquer longtemps à l’intérieur arrête donc toutes les autres tâches utilisant ce même objet protégé.
Traitements à éviter concrètement :
delayou des E/S lentes- des appels complexes vers un autre objet protégé
- des appels lourds à une bibliothèque externe
Notons que delay ou certaines E/S à l’intérieur d’une opération protégée ne relèvent pas d’un simple problème de performance : il s’agit d’une erreur limitée (bounded error) au sens de la norme Ada. Selon l’implémentation, cela peut provoquer un Program_Error ou un interblocage ; il faut donc les éliminer entièrement, et non simplement « limiter leur usage ».
Une bonne conception suit le schéma suivant : « extraire rapidement les valeurs nécessaires de l’objet protégé → effectuer les calculs lourds ou les E/S à l’extérieur → ne réécrire que le résultat, rapidement, dans l’objet protégé ».
Garder des conditions de barrière simples
La barrière entry ... when <condition> est puissante, mais devient difficile à lire si elle se complexifie trop, rendant ardu le diagnostic des raisons pour lesquelles une tâche n’est pas libérée.
L’idéal est un niveau où le sens de l’état se lit d’un coup d’œil, comme when Count < Buffer_Size ou when Used > 0. Si plusieurs conditions sont réellement nécessaires, envisagez de représenter l’état par un type énuméré, de manière à rapprocher la barrière d’une forme lisible par nom d’état, comme when State = Running.
Exceptions et arrêt des tâches
Il faut définir explicitement une politique pour le cas où une exception survient à l’intérieur d’une tâche. Au minimum, capturer l’exception au niveau le plus élevé du corps de la tâche et consigner ce qui s’est passé.
Plus important encore est la conception de ce qui se passe après l’exception. Le système peut-il continuer si cette tâche s’arrête ? Peut-on la redémarrer ? Comment prévenir les autres tâches ? Comment ramener l’état partagé à un état sûr ? Il faut être en mesure de répondre à ces questions. Ada fournit un mécanisme d’exceptions au niveau du langage, mais la sécurité après une exception relève de la responsabilité de la conception applicative.
Petite liste de vérification de conception
| Aspect | À vérifier |
|---|---|
| État partagé | Est-il enfermé dans un objet protégé ? Est-il manipulé directement depuis l’extérieur ? |
| Opérations protégées | Sont-elles courtes ? Bloquent-elles en interne ? |
| Entrées | La barrière est-elle simple ? Existe-t-il un risque d’attente indéfinie ? Une politique de délai d’expiration est-elle définie ? |
| Durée de vie des tâches | La condition de terminaison est-elle claire ? Une politique existe-t-elle en cas d’exception ? |
| Traitement périodique | Avez-vous envisagé delay until plutôt que delay ? |
En programmation concurrente, « ça devrait probablement aller » est la phrase la plus dangereuse. Rendre explicites dans le code l’état partagé, les conditions d’attente, les conditions de terminaison et la politique de gestion des exceptions est le premier pas vers une concurrence sécurisée.
12. Conclusion ── un langage qui a fait de la concurrence une « grammaire »
Ce qui distingue le modèle de concurrence d’Ada des autres langages, c’est que la concurrence sécurisée n’y est pas une « bonne pratique ajoutée après coup », mais une « grammaire » intégrée au langage.
| Ce que l’on veut faire | Syntaxe Ada |
|---|---|
| Unité d’exécution indépendante | task / task body |
| Communication synchrone | entry / accept |
| Attente de plusieurs requêtes | select / or / else |
| Exclusion mutuelle | protected / function / procedure |
| Synchronisation conditionnelle | entry ... when <barrier> |
| Délai d’expiration | or delay until <time> |
| Contrôle de priorité | pragma Priority |
Ces constructions syntaxiques sont soumises à la vérification du compilateur. Par exemple, tenter de modifier, à l’intérieur d’une fonction d’un objet protégé, les composants privés de cet objet lui-même provoque une erreur de compilation. Une fois qu’une opération protégée s’achève, les barrières des entrées en attente sont automatiquement réévaluées — aucun envoi de signal manuel n’est nécessaire.
« De même que le système de types garantit la sécurité mémoire,
la syntaxe de concurrence d'Ada garantit la sécurité de la synchronisation. »
Les huit exemples de code présentés dans cet article constituent une introduction pratique aux tâches, au rendez-vous, aux objets protégés et aux fonctionnalités temps réel. Une fois que vous les aurez exécutés par vous-même, essayez-vous aux sujets avancés suivants.
- Le profil Ravenscar : un profil de restriction des tâches destiné aux systèmes temps réel à haute fiabilité. Le modèle de tâches restreint permet une analyse statique des interblocages.
- Les blocs parallèles d’Ada 2022 : le traitement de données en parallèle grâce à la construction
parallel ... do. - L’intégration avec SPARK : la vérification formelle du comportement des programmes concurrents (prise en charge par GNATprove sous le profil Ravenscar).
« Utiliser Ada » ne suffit pourtant pas à garantir la sécurité
Une dernière remarque, importante. La syntaxe de concurrence d’Ada est puissante, mais utiliser Ada ne rend pas automatiquement un programme sûr. Manipuler directement des données partagées sans les placer dans un objet protégé, bloquer longtemps à l’intérieur d’un objet protégé, ou enchaîner des appels complexes entre plusieurs objets protégés — ces erreurs de conception restent possibles, même en Ada.
Les fonctionnalités du langage sont conçues pour qu’« écrire du code dangereux exige un effort explicite », mais elles ne se substituent pas à une conception correcte. La vraie valeur d’Ada est de rapprocher la discussion sur la sécurité du code lui-même : des questions comme « cet état est-il protégé ? », « quand cette tâche se termine-t-elle ? » ou « sous quelle condition cette entrée attend-elle ? » peuvent rester inscrites, sous forme syntaxique, directement dans le code.
La philosophie d’Ada, qui exprime la conception à travers les types, reste cohérente en matière de concurrence. Une concurrence sécurisée ne commence pas par une manipulation prudente des verrous, mais par le refus de laisser exister un état partagé dangereux, à nu.
À l’idée reçue selon laquelle « la concurrence est difficile », Ada répond : « choisissez la bonne syntaxe, et le compilateur garantira la sécurité ». Cette philosophie de conception trouve un écho dans des langages modernes comme Rust ou Pony, mais Ada la porte dans sa spécification depuis plus de 40 ans.
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Programmation de systèmes temps réel avec Ada ── Priorités, exécution périodique et contrôle du temps d'exécution en pratique
Découvrez l'Annexe D d'Ada (systèmes temps réel) à travers 8 exemples de code pratiques. L'article couvre progressivement les priorités d...
La programmation générique en Ada ── Écrire des contrats par les types, réaliser une réutilisation à coût nul
Une présentation systématique de la programmation générique en Ada : sous-programmes génériques, paquets génériques, paramètres formels d...
Introduction à la vérification formelle avec SPARK ── Des contrats Ada à la preuve mathématique
Une introduction pratique à la vérification formelle avec SPARK, le sous-ensemble d'Ada. L'article couvre le passage des contrats (Pre/Po...
L'attrait du langage Ada ── Exprimer la conception par les types, faire fonctionner des logiciels pendant des décennies
Une introduction à l'attrait du langage Ada : le typage fort, les contraintes de plage, la séparation de la spécification et de l'impléme...
Gestion des erreurs et stratégie de nouvelle tentative dans Power Automate — éviter qu'un flux qui fonctionnait ne s'arrête sans que personne ne le remarque
Un ensemble de modèles de conception pour éviter qu'un flux Power Automate ne s'arrête sans que personne ne s'en aperçoive. Nous détaillo...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Qu'est-ce qu'une tâche en Ada ?
- La tâche est l'unité de base de la concurrence en Ada. Elle ressemble à un thread, mais ne correspond pas nécessairement une à une à un thread du système d'exploitation : c'est le runtime Ada qui gère l'ordonnancement. Une tâche commence à s'exécuter automatiquement dès sa déclaration, et lorsque la procédure principale se termine, l'achèvement des tâches encore en cours d'exécution est attendu implicitement. Elle communique de façon synchrone avec l'extérieur via un rendez-vous passant par une entrée (entry). Les tâches et le rendez-vous font partie de la spécification du langage depuis Ada 83, en 1983.
- En quoi l'objet protégé d'Ada diffère-t-il d'un mutex ?
- L'objet protégé est un mécanisme d'exclusion mutuelle géré par le langage, qui dispense d'écrire manuellement le verrouillage et le déverrouillage. Une fonction (function) est en lecture seule et peut être appelée simultanément par plusieurs tâches ; une procédure (procedure) est en lecture-écriture et bloque tout autre appel pendant son exécution ; une entrée (entry) fait attendre l'appelant dans une file tant que sa condition de barrière n'est pas vraie. Le contrôle d'un tampon borné, qui en C nécessite de combiner un mutex pthread et une variable de condition, se résume en Ada à une seule ligne de barrière du type « when Count < Buffer_Size ».
- Comment fonctionne le rendez-vous en Ada ?
- Le rendez-vous est un mécanisme de communication synchrone entre tâches : l'appel d'entrée du côté appelant et l'instruction accept du côté de la tâche s'attendent mutuellement jusqu'à atteindre en même temps le point de rendez-vous. Les modes de paramètres in, out et in out permettent de transmettre des données dans les deux sens. Le bloc do...end du corps de l'accept constitue la section critique : pendant son exécution, l'appelant est bloqué et la tâche n'accepte aucune autre entrée. Combiné à l'instruction select, on peut écrire de façon déclarative l'attente de plusieurs entrées, des délais d'expiration et des conditions de garde.
- Que ne faut-il jamais faire à l'intérieur d'un objet protégé ?
- Tout traitement qui bloque longtemps : delay, entrées/sorties lentes, appels lourds à une bibliothèque externe, etc. Un delay ou certaines E/S à l'intérieur d'une opération protégée constituent une erreur limitée (bounded error) au sens de la norme Ada, et peuvent, selon l'implémentation, provoquer un Program_Error ou un interblocage : il faut donc les éliminer complètement. La règle d'or consiste à ne faire que de brèves mises à jour d'état, à exécuter les calculs lourds ou les E/S en dehors de l'objet protégé, et à n'y réécrire que le résultat, rapidement.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics