Une bonne conception est avant tout l'art de choisir la meilleure façon de répondre à un ensemble d'exigences. La
conception d'un bon système de concurrence repose souvent sur le fait de choisir la façon la plus simple de répondre au
besoin d'accès concurrent. Une des premières règles pour les concepteurs devrait être d'éviter de réinventer la roue.
De bons patterns de conception et des schémas de mise en oeuvre ont été développés pour résoudre la plupart des
problèmes. Etant donné la complexité des systèmes concurrents, il est logique de n'utiliser que des solutions déjà
avérées et de chercher une certaine simplicité dans la conception.
Les tâches concurrentes qui se déroulent entièrement à l'intérieur d'un ordinateur sont appelées unités
d'exécution. Comme toutes les tâches concurrentes, les unités d'exécution sont un concept abstrait puisqu'elles se
déroulent dans le temps. La meilleure façon d'enregistrer physiquement une unité d'exécution est de représenter son
état à un moment donné dans le temps.
La façon la plus directe de représenter des tâches concurrentes en utilisant des ordinateurs est de réserver un
ordinateur à chaque tâche. Toutefois, cette solution est généralement trop coûteuse et ne résout pas toujours les
conflits. Il est donc courant que le même processeur physique prenne en charge des tâches multiples, c'est ce que l'on
appelle un fonctionnement multitâche. Dans ce cas, le processeur et les ressources qui lui sont associées, comme
la mémoire et les bus, sont partagés. (Malheureusement, ce partage de ressources peut aussi créer de nouveaux conflits
inexistants dans le problème d'origine.)
La forme la plus courante de fonctionnement multitâche est de doter chaque tâche d'un processeur virtuel. Ce processeur
virtuel est généralement appelé processus ou tâche. Normalement, chaque processus a son propre espace adresse,
qui est logiquement distinct de l'espace adresse des autres processeurs virtuels. Cela empêche les conflits liés à
l'écrasement de la mémoire entre processus. Malheureusement, le temps système nécessaire pour basculer le processeur
physique d'un processus vers un autre est souvent prohibitif. Cela implique une importante permutation d'un ensemble de
registres à l'intérieur de l'unité centrale (changement de contexte) qui, même avec les processeurs modernes,
peut prendre des centaines de microsecondes.
Pour réduire ce temps système, beaucoup de systèmes d'exploitation offrent la possibilité d'inclure des unités
d'exécution simples à l'intérieur d'un processus unique. Les unités d'exécution à l'intérieur d'un processus
partagent l'espace adresse de ce processus. Cela réduit le temps système impliqué dans le changement de contexte mais
augmente la possibilité de conflits de mémoire.
Pour certaines applications à haut débit, même le temps système entraîné par la commutation d'unité d'exécution simple
peut être top élevé. Dans ces cas là, il est habituel d'avoir une forme encore plus légère de fonctionnement
multitâche, en tirant profit de certaines caractéristiques de l'application.
Les exigences de l'accès concurrent du système peuvent avoir un impact très important sur l'architecture du système. La
décision de transformer la fonctionnalité d'une architecture à processus unique en architecture à processus multiples
implique de grands changements dans la structure du système, sur plusieurs plans. Cela peut entraîner l'ajout de
mécanismes supplémentaires (comme les appels de procédure éloignée), ce qui changerait de manière importante
l'architecture du système.
Les exigences de la disponibilité du système doivent être prises en compte, tout comme le temps système supplémentaire
nécessaire pour gérer les processus supplémentaires et les unités d'exécution.
Comme la plupart des décisions concernant l'architecture, changer le processus d'architecture revient à échanger un
ensemble de problèmes contre un autre ensemble :
Approche
|
Avantages
|
Inconvénients
|
Processus unique, pas d'unité d'exécution
|
-
Simplicité
-
Messagerie interne rapide
|
-
Difficile d'équilibrer la charge de travail
-
Ne peut pas s'adapter à des processeurs multiples
|
Processus unique, plusieurs unités d'exécution
|
-
Messagerie interne rapide
-
Fonctionnement multitâches sans communication entre processus
-
Meilleur fonctionnement multitâche sans la surcharge de processus complexes.
|
-
L'application doit autoriser les unités d'exécution
-
Le système d'exploitation doit avoir une bonne gestion des unités d'exécution
-
Les problèmes liés à la mémoire partagée doivent être pris en compte
|
Processus multiples
|
-
S'adapte au fur et à mesure de l'ajout de processeurs
-
Relativement facile à distribuer grâce à des noeuds
|
-
Sensible aux frontières de processus : une trop grande utilisation de communication
inter-processus limite les performances
-
La permutation et les changements de contexte sont onéreux
-
Plus difficile à conceptualiser
|
On commence généralement avec une architecture à processus unique, puis on ajoute des processus pour les groupes de
comportements ayant besoin d'avoir lieu au même moment. A l'intérieur de ces regroupements plus larges, il faut penser
au besoin supplémentaire d'accès concurrents et ajouter des unités d'exécution à l'intérieur des processus pour
augmenter l'accès concurrent.
Il faut commencer par attribuer plusieurs objets actifs à une tâche unique ou à une unité d'exécution unique d'un
système d'exploitation, en utilisant le planificateur d'objet actif construit à cet effet. De cette façon, il est
généralement possible de réaliser une simulation très simple d'accès concurrent bien que, avec une tâche ou une unité
d'exécution unique d'un système d'exploitation, il ne sera pas possible de tirer profit de machines avec des unités
centrales multiples.La décision clé est d'isoler le comportement de blocage dans des unités d'exécution séparées, de
façon à ce qu'il ne devienne pas un goulot d'étranglement. De cette manière, les objets actifs avec des comportements
de blocage seront placés dans leurs propres unités d'exécution du système d'exploitation.
Dans les systèmes en temps réel, ce raisonnement s'applique aussi aux capsules : chaque capsule a une unité d'exécution
de contrôle logique qui peut partager ou non l'unité d'exécution, la tâche ou le processus d'un système d'exploitation
avec d'autres capsules.
Malheureusement, comme souvent en ce qui concerne l'architecture, il n'y a pas de réponse facile ; la bonne solution
implique une approche où tout aura été pris en compte. Des petits prototypes d'architecture peuvent être utilisés pour
voir les implications d'un ensemble de choix. En faisant un prototype de l'architecture du processus, veillez à ne pas
dépasser le nombre de processus théoriquement acceptés par le système. Prenez en compte les problèmes suivants :
-
Peut-on atteindre le nombre maximum de processus ? Jusqu'où le système peut-il être poussé ? Y a-t-il assez
d'espace pour une éventuelle croissance ?
-
Quelle conséquences y a-t-il à changer certains des processus pour simplifier les unités d'exécution qui
fonctionnent dans un espace adresse de processus partagé ?
-
Le temps de réponse est-il modifié à mesure que le nombre de processus augmente ? Les communications interprocessus
ont-elles augmenté ? Y a-t-il une dégradation notable ?
-
Pourrait-on réduire le nombre de communications interprocessus en regroupant ou en réorganisant des processus ? Un
changement de ce type donnerait-il de grands processus monolithiques difficiles à équilibrer ?
-
Les mémoires partagées peuvent-elles être utilisées pour réduire les communications interprocessus ?
-
Tous les processus devraient-ils bénéficier du même laps de temps lorsque les ressources temps sont attribuées ?
Est-il possible de reporter l'attribution de temps ? Y a-t-il des inconvénients à changer les priorités planifiées
?
Les objets actifs peuvent dialoguer entre eux de manière synchrone ou asynchrone. La communication synchrone est utile
car elle simplifie les collaborations complexes par un séquencement strictement contrôlé. C'est-à-dire que lorsqu'un
objet actif accomplit une étape d'exécution finale impliquant des appels synchrones d'autres objets actifs, toutes les
interactions concurrentes commencées par d'autres objets peuvent être ignorées jusqu'à ce que la séquence soit
terminée.
Si cela peut être utile dans certains cas, cela peut aussi poser des problèmes : il peut arriver, par exemple, qu'un
événement avec une priorité plus élevée doive attendre (inversion de priorité). Cela peut être exacerbé par la
possibilité que l'objet appelé de manière synchrone soit lui-même bloqué, attendant une réponse d'un appel synchrone de
son côté. Cela peut mener à une inversion illimitée des priorités. Dans le pire des cas, si la chaîne des appels
synchrones est circulaire, cela peut mener au blocage.
Les appels asynchrones évitent ce problème en autorisant les temps de réponse liés. Toutefois, selon l'architecture
logicielle, la communication asynchrone entraîne souvent une trop grande complexité du code : un objet actif peut avoir
à répondre à plusieurs événements asynchrones (lesquels peuvent entraîner une séquence complexe d'interactions
asynchrones avec d'autres objets actifs) à tous moments. Cela peut être très difficile à implémenter et entraîner des
erreurs.
L'utilisation d'une technologie de messagerie asynchrone avec une communication des messages certifiée peut simplifier
la tâche de programmation d'applications. L'application peut continuer l'opération même si la connexion réseau ou
l'application distante n'est pas disponible. On peut utiliser une messagerie asynchrone de manière synchrone. La
technologie synchrone aura besoin d'une connexion pour être disponible à chaque fois que l'application le sera. Comme
l'existence d'une connexion est connue, gérer un traitement obligatoire peut être plus simple.
Dans l'approche recommandée par le Rational Unified Process pour les systèmes en temps réel, les capsules dialoguent de manière asynchrone en utilisant des signaux, d'après des protocoles particuliers. Cependant, il est possible de réaliser une
communication synchrone en utilisant des paires de signaux, un dans chaque sens.
Bien que le temps système entraîné par le changement de contexte d'objets actifs puisse être très court, il est
possible que certaines applications trouvent encore ce coût inacceptable. C'est souvent le cas lorsqu'une grande
quantité de données doit être traitée à un haut débit. Lorsque cela arrive, il faut revenir à l'utilisation d'objets
passifs et à des techniques de gestion d'accès concurrent plus traditionnelles (mais plus risquées) comme les
sémaphores.
Cependant, cela n'implique pas forcément qu'il faille abandonner du même coup l'approche des objets actifs. Même dans
des applications de données intensives, il arrive souvent que la partie sensible aux performances représente une
portion relativement petite du système entier. Cela implique que le reste du système peut encore profiter du paradigme
objet actif.
En général, les performances ne sont qu'un des critères de conception quand il s'agit de la conception du système. Si
le système est complexe, d'autres critères comme la maintenabilité, la facilité d'effectuer des modifications et de
comprendre le système, par exemple, sont aussi, voire plus, importants. L'approche d'objets actifs a un gros avantage :
elle fait disparaître beaucoup de la complexité de l'accès concurrent et de sa gestion, tout en permettant à la
conception d'être exprimée en termes propres à l'application contrairement aux mécanismes de bas niveau spécifiques à
la technologie.
Les composants concurrents sans interactions sont presque un problème sans importance. Quasiment tous les problèmes
liés à la conception reposent sur des interactions entre des tâches concurrentes. Nous devons donc d'abord nous
concentrer sur la compréhension des interactions. Voici quelques questions à se poser :
-
L'interaction est-elle unidirectionnelle, bidirectionnelle ou multidirectionnelle ?
-
Existe-t-il une relation client-serveur ou maître-esclave ?
-
La synchronisation est-elle obligatoire ?
Une fois que l'interaction est comprise, on peut penser aux façons de l'implémenter. L'implémentation devrait être
choisie pour utiliser la conception la plus simple cohérente avec les objectifs de performance du système. Les
exigences de performance incluent généralement un bon rendement général et un temps d'attente acceptable dans les
délais de réponse des événements générés à l'extérieur.
Ces problèmes sont encore plus présents pour les systèmes en temps réel, généralement moins tolérants vis à vis des
variations de performance comme, par exemple, la gigue des temps de réponse ou des échéances non respectées.
Il ne faut pas incorporer des suppositions spécifiques sur des interfaces externes dans une application. Il est aussi
inutile d'avoir plusieurs unités d'exécution de contrôle bloquées attendant un événement. Au lieu de cela, attribuez à
un objet unique la tâche de détecter l'événement. Lorsque l'événement se produit, cet objet peut informer ceux qui en
ont besoin. Cette conception est basée sur un pattern de conception ayant fait ses preuves, le pattern "observateur"
[GAM94]. Pour une flexibilité encore plus grande, il peut facilement être transformé
en "Pattern diffuseur et souscripteur" dans lequel un objet diffuseur joue le rôle d'intermédiaire entre les détecteurs
d'événements et les objets intéressés par l'événement ("souscripteurs") [BUS96].
Dans un système, les actions peuvent être déclenchées par l'occurrence d'événements générés à l'extérieur. Un événement
très important généré à l'extérieur peut être simplement l'écoulement du temps, représenté par le tic-tac d'une
horloge. Les autres événements externes proviennent de dispositifs d'entrée connectés au matériel extérieur. Ils
incluent les dispositifs d'interface utilisateur, les détecteurs de processus et les liaisons avec les autres systèmes.
Cela est particulièrement vrai pour les systèmes en temps réel qui ont une grande connectivité avec le monde extérieur.
Pour qu'un logiciel identifie un événement, il doit, soit être bloqué en attente d'une interruption, soit contrôler
régulièrement le matériel pour voir si l'événement a eu lieu. Dans ce dernier cas, le cycle périodique doit être court
pour éviter de rater un événement à durée de vie courte ou des occurrences multiples ou encore simplement pour réduire
le temps d'attente entre l'occurrence d'un événement et sa détection.
Il est intéressant de remarquer que, peu importe la rareté de l'événement, certains logiciels doivent être bloqués pour
l'attendre ou contrôler régulièrement s'il a eu lieu. Mais la plupart (si ce n'est la totalité) des événements qu'un
système doit décrire sont rares ; la plupart du temps, dans la plupart des systèmes, rien n'ayant une grande importance
ne se produit.
Le fonctionnement d'un ascenseur illustre bien cela. Parmi les événements importants de la vie d'un ascenseur on trouve
: être appelé par une personne, le choix de l'étage par la personne, une personne bloquant la porte avec sa main et le
fait d'aller d'un étage à un autre. Certains de ces événements impliquent un temps de réponse très court, mais ils sont
tous très rares comparés à l'échelle de temps du temps de réponse désiré.
Un événement unique peut déclencher plusieurs actions et les actions peuvent dépendre de l'état de différents objets.
De plus, les différentes configurations d'un système peuvent utiliser le même événement différemment. Par exemple,
lorsqu'un ascenseur passe un étage, l'écran dans la cabine de l'ascenseur doit être mis à jour et l'ascenseur lui-même
doit savoir à quel étage il est pour pouvoir répondre aux nouveaux appels et aux choix des étages par les personnes. Il
peut y avoir ou non des écrans de localisation d'ascenseur à chaque étage.
L'interrogation coûte chère : une partie du système doit régulièrement arrêter ce qu'elle est en train de faire pour
vérifier si un événement a eu lieu. Si on doit répondre rapidement à l'événement, le système devra contrôler l'arrivée
des événements assez fréquemment, limitant encore plus la quantité de travail pouvant être accomplie.
Il est beaucoup plus efficace d'attribuer une interruption à l'événement, le code dépendant de l'événement étant alors
activé par l'interruption. Même si les interruptions sont parfois évitées car jugées trop chères, les utiliser de façon
judicieuse peut être beaucoup plus efficace que des interrogations à répétition.
Il est préférable de choisir les interruptions comme mécanisme de notification d'événements lorsque l'arrivée des
événements est aléatoire et occasionnelle, entraînant que la plupart des efforts d'interrogation concluent que
l'événement n'a pas eu lieu. Il est préférable de choisir les interrogations lorsque les événements arrivent de manière
régulière et prévisible. De cette manière, les efforts d'interrogation détectent que l'événement a eu lieu. Entre les
deux, il existe un point où choisir les interruptions ou les interrogations revient au même : les deux feront aussi
bien et le choix importe peu. Cependant, dans la plupart des cas, étant donné le caractère aléatoire des événements
dans le monde réel, mieux vaut choisir un comportement réactif.
La diffusion de données (surtout en utilisant des signaux) est onéreuse, et très peu rentable : seuls quelques objets
peuvent être intéressés par les données mais tous (ou beaucoup) doivent s'arrêter pour l'examiner. Une meilleure
approche, consommant moins de données, est d'utiliser la notification pour n'informer que les objets intéressés par le
fait qu'un événement ait eu lieu. Limitez la diffusion d'événements demandant l'attention de nombreux objets (comme les
événements liés au rythme ou à la synchronisation).
Plus précisément :
-
Utilisez les objets passifs et les appels de méthode synchrones lorsque l'accès concurrent n'est pas un problème
mais que la réponse instantanée l'est.
-
Utilisez les objets actifs et les messages asynchrones pour la plupart des concepts d'accès concurrent au niveau
des applications.
-
Utilisez les unités d'exécution du système d'exploitation pour isoler les éléments de blocage. Un objet actif peut
être mappé à l'unité d'exécution d'un système d'exécution.
-
Utilisez les processus du système d'exploitation pour une isolation maximum. Des processus séparés sont nécessaires
si des programmes ont besoin d'être démarrés et fermés de manière indépendante. Il en va de même si des
sous-systèmes doivent être distribués.
-
Utilisez des unités centrales séparées pour la distribution physique et pour la puissance brute.
L'instruction la plus importante concernant le développement d'applications concurrentes efficaces est peut-être
d'utiliser au maximum les mécanismes d'accès concurrent les plus simples. Le matériel et le logiciel du système
d'exploitation jouent un rôle primordial dans la prise en charge de l'accès concurrent, mais ils ont des mécanismes
relativement complexes, laissant une grande partie du travail au concepteur de l'application. Il faut combler l'écart
existant entre les outils disponibles et les besoins des applications concurrentes.
Les objets actifs aident à combler cet écart grâce à deux options clés :
-
Ils unifient les abstractions du concept en encapsulant les unités de base de l'accès concurrent (une unité
d'exécution de contrôle) qui peuvent être implémentées en utilisant un des mécanismes sous-jacents fournis par le
système d'exploitation ou par l'unité centrale.
-
Lorsque des objets actifs partagent une unité d'exécution unique du système d'exploitation , ils deviennent un
mécanisme d'accès concurrent à la fois simple et efficace, qui devrait autrement être implémenté directement dans
l'application.
Les objets actifs sont aussi un environnement idéal pour les objets passifs fournis par les langages de programmation.
Concevoir un système à partir d'objets concurrents, sans produits procéduraux comme des programmes ou des processus,
amène à une conception plus modulaire, cohésive et compréhensible.
Dans la plupart des systèmes, moins de 10 % du code utilise 90 % des cycles de l'unité centrale.
La plupart des concepteurs de système agissent comme si chaque ligne de code devait être optimisée. Au lieu de cela,
optimisez les 10 % du code les plus souvent exécutés ou les plus longs. Conceptualisez les 90 % restants en mettant
l'accent sur la modularité et sur la facilité de compréhension et d'implémentation.
Les exigences non fonctionnelles et l'architecture du système influenceront le choix des mécanismes utilisés pour
implémenter les appels de procédure éloignée. Vous trouverez ci-dessous une vue d'ensemble des types de compromis
entre les différentes alternatives :
Mécanisme
|
Utilisations
|
Commentaires
|
Messagerie
|
Accès asynchrone aux serveurs des entreprises
|
Une messagerie middleware peut simplifier l'application de programmation de tâches en se chargeant de
la file d'attente, du délai d'attente et des conditions de reprise et de redémarrage. Vous pouvez aussi
utiliser une messagerie middleware dans un mode pseudo-synchrone. En général, la technologie de
messagerie peut prendre en charge des messages de grande taille. Certaines sortes d'appels de procédure
éloignée peuvent être limitées au niveau de la taille des messages et donc avoir besoin de
programmation supplémentaire pour gérer les grands messages.
|
Connectivité JDBC et interface ODBC
|
Appels de bases de données
|
Ce sont des interfaces indépendantes des bases de données pour les servlets Java ou les programmes
d'applications, qui servent à effectuer des appels vers des bases de données pouvant être sur le même
serveur ou sur un serveur différent.
|
Interfaces natives
|
Appels de bases de données
|
Beaucoup de fournisseurs de bases de données ont implémenté des interfaces de programmation natives
dans leurs propres bases de données, ce qui permet d'augmenter les performances par rapport au
protocole ODBC au détriment de la portabilité de l'application.
|
Appel de procédure éloignée
|
Appel de programmes sur des serveurs distants
|
Vous n'avez peut-être pas besoin de programmer les appels de procédure éloignée si un générateur
d'application s'en occupe pour vous.
|
Interactif
|
Peu utilisé dans les applications e-business
|
En général, communication peu évoluée de programme à programme utilisant des protocoles comme APPC ou
Sockets.
|
De nombreux systèmes ont besoin d'un comportement concurrent et de composants répartis. La plupart des langages de
programmation ne nous aident que très peu concernant ces problèmes. Nous avons vu qu'il faut un grand pouvoir
d'abstraction pour comprendre à la fois le besoin d'accès concurrent dans les applications et les options pour les
implémenter dans le logiciel. Nous avons aussi vu que, paradoxalement, si les logiciels concurrents sont plus complexes
que les logiciels non concurrents, ils peuvent aussi grandement simplifier la conception des systèmes qui doivent gérer
des accès concurrents dans le monde réel.
|