Modèles, méthodes et outils Concurrence
Les modèles classiques
Les deux modèles de programmation concurrente classiques sont ceux qui ont été conçus pour créer les systèmes d’exploitation, et sont maintenant utilisés par de nombreux langages plus haut niveau, permettant de programmer avec des processus légers, comme Java. Ceux-ci devaient être capable de gérer l’exécution en parallèle de plusieurs processus (correspondant généralement à plusieurs applications diérentes). Deux écoles se sont donc longtemps opposées, le modèle préemptif et le modèle coopératif.
Le modèle préemptif
Dans ce modèle, dont le meilleur exemple d’utilisation est le système d’exploitation Unix et la norme POSIX, c’est l’ordonnanceur qui décide quand l’exécution d’une tâche doit être interrompue pour laisser place à l’exécution d’une autre tâche. Le programmeur doit donc sécuriser l’accès aux ressources qui risquent d’être utilisées simultanément par les mêmes tâches : il ne peut pas savoir a priori à quel moment de l’exécution la tâche qu’il est en train de programmer sera interrompue. Si celle-ci est justement en train d’écrire dans un chier ou de modier une donnée, et que la tâche prenant la main a justement besoin de modier ou de lire ces mêmes données, les résultats de l’exécution peuvent être pour le moins surprenants… Pour sécuriser ces accès, on peut par exemple utiliser des systèmes à base de verrous : avant l’accès à une ressource potentiellement partagée, le programmeur d’une tâche place un verrou sur cette ressource, indiquant ainsi à l’ordonnanceur qu’il faut bloquer les autres tâches au point de leur exécution où elles ont besoin d’utiliser cette ressource. Lorsque le programmeur de la tâche décide qu’il n’a plus besoin de modier la ressource, il supprime le verrou, ce qui indiquera à l’ordonnanceur qu’il peut donner la main à l’une des tâches en attente de cette ressource. Ce modèle est dicile à utiliser pour le programmeur, qui doit être particulièrement attentif à l’utilisation des ressources partagées, comme le sou- ligne la littérature à ce sujet. Par exemple, des patrons de conception (design patterns) complexes pour la gestion de la synchronisation dans ce modèle peuvent être trouvés dans [90]. De plus, un programme peut sembler fonctionner parfaitement jusqu’au jour où une conguration particulière fait que deux tâches bloquent chacune en attendant que l’autre lève le verrou. Les programmes utilisant ce système sont diciles à tester, d’autant plus que la plupart des ordonnanceurs ne distribuent pas le temps de calcul entre les tâches de manière déterministe (voir glossaire), ou même simplement reproductible. Malgré ces inconvénients, c’est le modèle le plus utilisé par les systèmes d’exploitations et par la plupart des langages modernes.
Le modèle coopératif
L’approche coopérative a été utilisée par les premiers systèmes d’exploitation grand-public (les premières versions de Windows et de Mac OS utilisaient ce modèle pour faire fonctionner simultanément plusieurs applications). Dans ce modèle, ce n’est pas l’ordonnanceur, mais la tâche elle-même qui dit quand elle est prête à interrompre son exécution. C’est donc le programmeur qui place l’instruction permettant de donner l’autorisation à l’ordonnanceur d’interrompre son exécution pour reprendre celle d’une autre tâche. Un des avantages de ce modèle est qu’il présente un modèle de programmation plus simple que le modèle préemptif, l’accès aux ressources partagées entre les diérentes tâches n’ayant pas besoin d’être synchronisé par des mécanismes comme les verrous. Il est bien moins facile de bloquer totalement l’application comme c’est le cas avec le modèle préemptif. C’est toujours possible, mais il sera plus facile de détecter le problème. Un autre des avantages du modèle coopératif est aussi son principal inconvénient : le passage de l’exécution d’une tâche à une autre est coûteuse, car l’ordonnanceur doit procéder à un changement de contexte, pour stocker le contexte d’exécution de la tâche qui s’interrompt et restaurer celui de la tâche qui reprend son exécution. Dans le modèle préemptif, le programmeur ne peut pas éviter les changements de contexte inutiles provoqués par l’ordonnanceur, tandis que dans le modèle coopératif, il décide lui-même du moment où le changement de contexte est nécessaire. Cela signie qu’en règle générale, on peut écrire des programmes beaucoup plus ecaces avec le modèle coopératif qu’avec le modèle préemptif. Mais cela signie également que le développeur maladroit peut produire une application particulièrement inecace en prenant de mauvaises décisions de conception. Ce modèle a néanmoins été presque complètement délaissé pour la conception des systèmes d’exploitation, car il est généralement considéré que le système est le mieux placé pour déterminer quand il est nécessaire de procéder à un changement de la tâche active, selon les informations dynamiques qu’il possède sur les applications en cours et leurs priorités d’exécution, et surtout parce qu’il n’est pas considéré comme raisonnable qu’une application mal développée pour le système d’exploitation puisse dégrader ses performances. Cependant, nous considérons que dans le cadre d’un langage haut-niveau ou d’un modèle de framework, mis entre les mains de développeurs compétents, les avantages de l’approche coopérative l’emportent sur ses inconvénient.