Pourquoi créer des nouveaux langages de programmation ?
Chaque année, de nouveaux langages de programmation font leur apparition. Permettant d’écrire des programmes, c’est-à-dire de communiquer avec un ordinateur, ils répondent aux mêmes règles que les langages écrits : ils sont définis par une syntaxe, une grammaire et une sémantique. Pour simplifier, un programme est une liste d’instructions écrites destinée à l’ordinateur, ressemblant un peu à une recette de cuisine. Un langage de programmation correspond alors à la langue et au style utilisés pour écrire les recettes.
Pourquoi créer de nouveaux langages ? Écrire des programmes sans erreur relève quasiment de l’impossible (voir l’article Demandez le programme) et créer de meilleurs langages — nous verrons juste après ce que « meilleur » signifie — est une façon de résoudre ce problème. Pour être « optimal », un langage de programmation doit remplir les trois critères suivants :
- Expressivité : Il est parlant, c’est-à-dire qu’il permet d’écrire tous les programmes sans difficulté, et par conséquent de diminuer le temps passé à les créer, ou de faciliter le raisonnement du programmeur afin de limiter les erreurs. Si on en revient à l’analogie des recettes de cuisine, un bon langage comprendra des instructions fréquemment utilisées comme « faire revenir » ou « monter les blancs en neige ». Un vocabulaire spécifique aide en général à écrire des instructions claires.
- Sûreté : Il assure le comportement correct des programmes, afin de garantir une bonne exécution du programme. Cela peut être fourni par la grammaire du langage ou par une vérification additionnelle. Un bon langage de recette de cuisine donne l’assurance qu’aucun travail n’est effectué pendant que l’on fait revenir des aliments pour ne pas les faire brûler.
- Efficacité : Il permet d’exécuter les programmes, soit en transformant les programmes afin qu’ils s’exécutent rapidement, soit en permettant au programmeur d’exprimer des optimisations. En cuisine, un langage efficace permettrait par exemple de décrire précisément quels ustensiles employer pour réaliser rapidement une recette : par exemple, utiliser un fouet électrique pour monter les œufs en neige mais pas pour faire une omelette.
Ces objectifs sont souvent contradictoires, les langages favorisant l’un ou l’autre de ces critères, ce qui explique que l’on crée toujours de nouveaux langages pour trouver de nouveaux compromis ou pour s’adapter à un domaine d’application. Par exemple les langages ayant un plus haut niveau d’abstraction, c’est-à-dire où chaque instruction déclenche un grand nombre de tâches, limiteront souvent le nombre d’erreurs et faciliteront la vérification des programmes, parfois au détriment de la performance.
Revenons à notre recette pour mieux comprendre ce compromis. Si on prend par exemple un robot multifonction programmable, un de ceux qui font presque toute la recette pour vous. Pour un tel robot, une recette est plus sûre (il est plus difficile de rater sa recette), plus rapide (sauf peut-être pour un cuisinier expert), mais aussi plus restreinte (il va falloir encore attendre de nombreuses années pour qu’un robot de cuisine fasse des macarons). Le robot programmable a ainsi un langage de programmation efficace et sûr mais peu expressif.
Pourquoi autant d’erreurs dans les programmes ?
Programmer sans fautes est difficile car il s’agit d’écrire une série d’instructions précises pour résoudre un problème. En quelque sorte, programmer c’est prévoir précisément ce qui peut se passer lorsque l’on demande à l’ordinateur d’exécuter une tâche, et il est facile d’oublier certains cas. Pour reprendre l’analogie de la cuisine, un être humain peut se contenter d’une série d’instructions moins précises qu’un ordinateur car il s’adapte (si la recette ne mentionne pas qu’il faut casser les œufs, il y pensera), et il se rendra compte qu’il est anormal de mélanger les coquilles à la pâte.
Un ordinateur est capable d’exécuter plusieurs tâches à chaque instant. Ainsi, on peut s’en servir pour écouter de la musique tout en saisissant un texte. Plus généralement, les ordinateurs peuvent être utilisés en réseau, soit pour se partager les tâches et effectuer des calculs plus importants, soit pour accéder à des informations distantes ou supplémentaires. Par exemple, les phases d’apprentissage de l’intelligence artificielle reposent sur l’exploration parallèle d’un grand nombre de situations. Un langage de programmation pour le parallélisme doit permettre d’exprimer aussi bien la création de tâches que leur coordination et leurs interactions. Pour insister sur les conflits entre les différentes tâches, on parle parfois de programmes concurrents.
Dans le cas de la programmation parallèle, c’est encore plus compliqué de programmer sans fautes car il faut non seulement prévoir les conditions d’exécution mais aussi l’ordonnancement possible entre les différentes tâches. Jusqu’ici on supposait que notre cuisinier travaillait seul, mais s’il travaille en équipe, il devra se synchroniser avec ses collègues. Si la sauce dont il a besoin est encore en préparation, le cuisinier pourra avoir lui-même l’idée de faire autre chose en attendant ; en informatique, il faut planifier quoi faire en cas d’attente au moment où on écrit le programme. Prenons notre robot programmable : si le programme est à l’étape « mesurer 100 g de sucre », il sera difficile de convaincre mon robot que « je peux d’abord mettre les œufs, cela revient au même, on mesurera le sucre plus tard », sauf bien sûr si cela avait été prévu dans le programme.
Les programmes parallèles ont leurs erreurs spécifiques, en plus de celles évoquées ci-dessus. Parmi elles, les deux erreurs principales sont :
- les accès concurrents à une donnée, lorsque deux tâches essayent d’accéder à une même donnée de façon contradictoire car il est difficile de déterminer l’état de la donnée après un tel accès concurrent,
- les interblocages, lorsque plusieurs tâches attendent mutuellement une action de l’autre pour progresser car le programme ne se termine jamais.
Si l’on revient à notre comparaison culinaire, l’accès concurrent correspondrait à un cuisinier essayant de se servir de la salière pendant qu’un collègue la remplit. A priori il est difficile de savoir si le sel sera versé sur la table, ou si le cuisinier attendra le retour de la salière ou encore s’il ira se plaindre au directeur. Un interblocage correspondrait quant à lui à deux cuisiniers s’attendant mutuellement, l’un ayant besoin du sel pour faire sa sauce, l’autre ayant déjà pris la salière et attendant la sauce pour finir son plat avant de libérer la salière. Les ordinateurs ont plus de mal que les cuisiniers à négocier pour trouver une solution dans une telle situation. Il existe plusieurs moyens de se prémunir de ces erreurs dans les langages concurrents mais cela est soit difficile soit restrictif en termes d’expressivité, surtout si l’on souhaite supprimer les deux sources d’erreurs en même temps.
La programmation par objets actifs
Les langages à objets actifs forment une famille de langages qui permettent d’écrire des programmes parallèles. Ils ont pour but de faciliter l’écriture de programmes parallèles en éliminant les accès concurrents aux données. Par contre, on n’élimine pas les interblocages en considérant que cela limiterait trop l’expressivité, mais il est possible d’analyser les programmes pour trouver la plupart des interblocages.
Le nom « programmation par objets actifs » provient de l’hybridation de deux idées plus anciennes dans le domaine des langages. Tout d’abord, la programmation orientée objet est un paradigme connu : un objet est une structure constituée de données et des fonctions travaillant sur ces données. Les objets actifs sont en fait l’adaptation à la programmation orientée objet, d’un paradigme de programmation plus ancien : les acteurs. Pour éviter les accès concurrents aux données, les objets actifs associent une tâche à chaque donnée, seule cette tâche a le droit de manipuler la donnée. Pour interagir, les tâches s’envoient des messages. C’est comme si nos cuisiniers pouvaient se parler mais chacun restant dans sa zone de travail, tout le matériel dont ils ont besoin y étant stocké : il est interdit d’utiliser ou de remplir la salière qui est chez quelqu’un d’autre, mais un cuisinier peut demander à un autre de la lui transmettre.
Comme une tâche ne fait qu’une seule chose à la fois, elle traitera les messages les uns après les autres et on utilise une boîte aux lettres pour stocker les messages en attente (la « todo list » du cuisinier). Un objet actif est à la fois un objet et une tâche associée à cet objet ; il communique en envoyant des messages aux autres objets actifs ; chaque message demande à l’objet cible d’exécuter une de ses fonctions. Pour rendre plus efficace le dialogue et s’assurer que le dialogue se déroule correctement entre chaque cuisinier, chacun a une liste de recettes qu’il sait réaliser, les seuls échanges possibles dans la cuisine sont donc des noms de recettes à exécuter ainsi que des données (ingrédients/plats/sauces/…). Comme l’interaction est bien définie, on peut garantir que l’on demandera toujours à un cuisinier une recette qu’il maîtrise.
Ainsi, chaque objet actif a un « type » et on ne peut pas demander à un objet de réaliser une fonction qu’il ne connaît pas. De plus, on a une garantie sur la nature de ce que l’on obtient. Cette « garantie du résultat » nous amène à une dernière notion : les futurs. Un futur est une promesse de réponse, dans notre cas un objet vide représentant le résultat d’une fonction qui sera exécutée plus tard par un objet actif. Lorsque l’on demande à un cuisinier de réaliser une recette, disons une sauce par exemple, on obtient immédiatement un récipient vide que l’on peut déplacer ou donner à un autre cuisinier. Ensuite, quand la sauce sera prête, le récipient se remplira automatiquement, où qu’il soit ! Si un cuisinier a besoin de la sauce et qu’il se rend compte que le récipient est vide, alors il attend.
Plusieurs langages à objets actifs existent et sont actuellement utilisés, certains plus industriels (par exemple Orleans, Akka), d’autres plus académiques (par exemple ABS, ASP, Encore et Rebeca). Avec plusieurs contributeurs de ces langages, nous avons publié une étude comparant les langages à objets actifs. Cette étude s’adresse aux programmeurs désireux de comprendre les différences entre les langages à objets actifs et de choisir celui qui est le plus adapté à leurs besoins, mais aussi aux concepteurs de langage qui y trouveront une vision d’ensemble sur les choix de conception et d’implémentation possibles. Ces langages se distinguent par le compromis qu’ils proposent entre expressivité, sûreté et efficacité. En particulier, certains langages exposent les programmeurs à plus de notions spécifiques de la programmation parallèle (comme les futurs) que d’autres. Cacher ces notions aux programmeurs facilite la programmation de tâches simples mais rend la compréhension des erreurs et leur analyse plus difficile.
De nouveaux horizons
Pour conclure, la recherche sur les langages de programmation permet d’explorer de nouveaux compromis d’expressivité (pour écrire des programmes complexes) et de sûreté (pour éviter des bugs), mais aussi de créer des langages dédiés à certaines applications (le traitement de grandes masses de données par exemple). Le parallélisme et la concurrence permettent de réaliser des tâches en parallèle ou d’utiliser plusieurs ordinateurs pour un même programme. Cela permet de faire des calculs plus rapidement ou avec plus d’informations. Mais cela génère aussi de nouveaux risques d’erreurs. Plusieurs familles de langages comme les objets actifs ont pour objectif de limiter la concurrence pour faciliter la programmation, et ainsi limiter le nombre d’erreurs quitte à être parfois un peu moins efficaces. Les objets actifs facilitent la programmation des tâches parallèles en évitant les accès concurrents aux données ; des outils de vérification existent pour analyser la présence d’interblocages dans les programmes. Bien sûr, l’analogie des recettes de cuisine a ses limites, par exemple il est plus facile de dupliquer des données que des sauces !
Bibliographie :
- Frank De Boer, Vlad Serbanescu, Reiner Hähnle, Ludovic Henrio, Justine Rochas, Crystal Chang Din, Einar Broch Johnsen, Marjan Sirjani, Ehsan Khamespanah, Kiko Fernandez-Reyes, and Albert Mingkun Yang. 2017. A Survey of Active Object Languages. ACM Comput. Surv. 50, 5, Article 76 (October 2017), 39 pages.
- Denis Caromel and Ludovic Henrio. 2005. A Theory of Distributed Object. Springer.
- Einar Broch Johnsen, Reiner Hähnle, Jan Schäfer, Rudolf Schlatte, and Martin Steffen. 2011. ABS: A Core Language for Abstract Behavioral Specification. In Formal Methods for Components and Objects (FMCO’10).
- Stephan Brandauer, Elias Castegren, Dave Clarke, Kiko Fernandez-Reyes, Einar Broch Johnsen, Ka I. Pun, S. Lizeth Tapia Tarifa, Tobias Wrigstad, and Albert Mingkun Yang. 2015. Parallel Objects for Multicores: A Glimpse at the Parallel Language Encore. In Formal Methods for Multicore Programming: 15th Intl. School on Formal Methods for the Design of Computer, Communication, and Software Systems.
- Marjan Sirjani, Frank S. de Boer, and Ali Movaghar-Rahimabadi. 2005. Modular Verification of a Component-Based Actor Language. Journal of Universal Computer Science 11, 10 (2005).
- Derek Wyatt. 2013. Akka Concurrency. Artima.
- Philip A. Bernstein and Sergey Bykov. 2016. Developing Cloud Services Using the Orleans Virtual Actor Model. IEEE Internet Computing 20, 5 (2016).
Newsletter
Le responsable de ce traitement est Inria. En saisissant votre adresse mail, vous consentez à recevoir chaque mois une sélection d'articles et à ce que vos données soient collectées et stockées comme décrit dans notre politique de confidentialité
Niveau de lecture
Aidez-nous à évaluer le niveau de lecture de ce document.
Votre choix a été pris en compte. Merci d'avoir estimé le niveau de ce document !
Ludovic Henrio
Chercheur CNRS, membre de l'équipe CASH au Laboratoire de l'Informatique du Parallélisme (LIP).