xavier-van-de-woestyne-initial
Xavier Van de Woestyne
Functional Programmer

La concurrence en Go et Erlang

Architecture, Erlang, Go, Programmation fonctionnelle
19/10/2016

Go et Erlang sont deux langages souvent comparés, dans cet article, nous comparons la manière que les deux langages ont de gerer la concurrence.

Go et Erlang, deux approches de la concurrence différentes

Erlang et Go partagent traits, y compris dans leur traitement de la concurrence, cependant, ils possèdent des approches très différentes. (En plus des différences dans la manière de traiter la concurrence, les langages sont très différents par leurs syntaxes, leurs éco-systèmes respectifs et le fait que Erlang est un langage fonctionnel alors que Go propose des mécanismes plus proches de la programmation impérative, bien qu'il soit dôté de fonctions anonymes.)

Commençons par évoquer rapidement les points fondamentaux de leur approche de la concurrence.

Mise à jour suite au commentaire de lcoullet

Actuellement, Elixir bénéficie d'une popularité croissante, de ce fait, certaines personnes s'interrogent, à juste titre, de l'intérêt de comparer Go et Erlang plutôt que Go et Elixir (qui est plus accessible). Dans l'article, nous parlons de la manière dont Erlang, via sa machine virtuelle gère la concurrence, de ce fait, à peu près tout ce qui est dit concernant Erlang peut s'appliquer à Elixir.

Erlang

  • Le langage compile un byte-code pour une machine virtuelle dédiée ;
  • les entitées concurrentes sont des processus ;
  • les processus sont identifiés par un ID unique (un PID) ;
  • les messages sont non-typés ;
  • les processus ne partagent pas de mémoire ou de données entre eux, autres que des messages ;
  • les variables sont immutables ;
  • les processus peuvent crasher (à cause d'un message inattendu par exemple ou du lancement d'une exception) ;
  • les processus sont ordonnancés par un arbre de supervision ;
  • absence de Mutex ou de Sémaphores ;
  • des comportements (et génération de code).

Concrètement, dès que l'on veut exécuter une action concurrente, il suffit de lancer une fonction dans un processus. Cette démarche renverra comme expression le PID du processus. Il devient donc possible de lui envoyer des messages. Une fonction de processus est écrite comme une boite au lettre, qui attend des messages et qui effectue de la correspondance de motifs pour choisir quelle action effectuer par message reçu. Comme les messages ne sont pas typés, il est possible d'envoyer n'importe quelle expression Erlang comme un message.

Comme les variables sont immutables et que les processus ne partagent rien d'autre que des messages, il n'est pas nécéssaire d'implémenter (ou d'utiliser) les outils de verrouillage de ressources (Mutexes et sémaphores), ce qui rend l'implémentation de systèmes multi-processus très simple.

Le fait qu'un processus puisse crasher n'est pas un vrai problème car tous les processus sont ordonnés dans un superviseur qui peut relancer (ou non) des processus morts.
En plus de primitives très simples, Erlang et sa machine virtuelle offrent OTP, pour Open Telecom Platform (ce nom n'est plus tout à fait pertinent car Erlang n'est plus utilisé uniquement pour la téléphonie, cependant, son nom est resté tel quel). OTP est une collection de bibliothèques qui permet la manipulation de structures de données (par exemples des graphes dirigés) ou de services (par exemple une base de données clé-valeur). En plus de ces outils, OTP offre une base structurelle pour la réalisation de code concurrent. Ces bases sont présentées sous la forme d'interface minimaliste à implémenter pour résoudre des problématiques précises, on appelle ces fragments des comportements et ils s'intégrent parfaitement dans les arbres de supervision.
Les comportements offerts dans la bibliothèque standard sont :

  • gen_server : pour l'implémentation de serveurs concurrents ;
  • gen_fsm : pour l'implémentation de machines à état fini ;
  • gen_statem : pour l'implémentation de machines à état ;
  • gen_event : pour l'implémentation de canons à événéments ;
  • supervisor : pour l'implémentation de son propre arbre de supervision.

En général, les programmes sont reliés dans un dernier comportement : application, qui offre une glue pour lier entre des composantes logicielles et des superviseurs.

Go

  • Le langage compile vers un code natif ;
  • les entités concurrentes sont des goroutines ;
  • les goroutines communiquent au moyen de channels ;
  • les goroutines partagent leur mémoire ;
  • les variables sont mutables.

Une goroutine permet d'exécuter une fonction en concurrence avec le thread courant. Les goroutines fournissent une abstraction sur les threads et le langage (et son contexte d'exécution) tire parti de la topologie de l'ordinateur qui exécute le programme pour choisir la stratégie de simultanéïté idéale. Donc une goroutine ne consommera pas spécialement un nouveau thread.

Pour communiquer, il est courant de modifier des variables globales ou d'écrire (ou de lire) un channel dont le type est connu à la compilation, ce qui implique que la structure des messages sera toujours connue. Un channel peut être lu/écrit par toutes les goroutines qui y ont accès (via la portée lexicale).

Le fait que la mémoire soit partagée et que les goroutines aient la capacité de communiquer avec le monde extérieur implique parfois de devoir écrire ses propres exclusions mutuelles et de manipuler des sémaphores. Cependant, la bibliothèque standard de Go est très riche et offre beaucoup d'outils pour manipuler les problèmes liés aux partages de mémoire.

En plus de la notion d'acteur, qui en Go, n'est pas implicite, Go repose sur la communication séquentielle de processus, ce qui rend la programmation concurrente réalisable, mais peut être un peu moins abordable qu'en Erlang.

Différences fondamentales entre Erlang et Go

En évoquant les points qui semblent caractériser la concurrence dans les deux langages, on se rend vite compte que Erlang offre plus d'outils pour rendre le traitement concurrents de fonctions plus aisés. Concrètement, le programmeur Go devra écrire plus de code pour démarrer un projet qui manipule des éléments concurrents que le programmeur Erlang.

En effet, Erlang impose rapidement des motifs (les comportements) à respecter et par extension, impose une structure pour organiser son code. L'absence de vérification statique de types permet aussi de restreindre la structure des messages traités par les acteurs, il est possible d'envoyer n'importe quel terme Erlang.

En Go, le développeur est moins aidé. Il doit construire lui même sa propre architecture, manipulant l'ensemble des composants de la bibliothèque standard (ou de paquets externes correctement importés). De plus, comme chaque processus n'est pas enfermé dans une boite noire (à la manière des processus de Erlang), le développeur doit veiller à correctement manipuler sa mémoire, l'accès aux ressources et le partage d'état. Comme la communauté Go est très active, il existe tout de même un grand nombre d'outils pour organiser le code et, par exemple, intégrer directement une logique d'envoi et de réception de messages.

Le typage statique de Go rend l'expressivité de la communication entre des processus et d'autres états plus restreinte, cependant, cela amène aussi une vérification statique (au moment de la compilation) plus fine, anticipant plus de bogues potentiels.

Conclusion : Go, Erlang, who wins !

Rob Pike (un des créateurs de Go) propose une analogie intéressante entre la concurrence en Go et la concurrence en Erlang :

  • Ecrire un fichier via son nom : Erlang (où le nom est un PID) ;
  • écrire un fichier via son FileDescriptor : Go (où le FileDescriptor est un channel).

Chez Dernier Cri, nous pensons qu'il n y a pas de gagnant à proprement parler. En effet, les deux méthodes offrent des avantages.

Si vous désirez apprendre la programmation concurrente, Erlang sera un choix judicieux car tout son écosystème est pensé pour aider le développeur à construire des applicatons concurrentes. En effet, il est plus simple d'utiliser une API qui reprend chaque fois le nom du fichier qu'une API qui impose le fait de relayer son descripteur de fichier. Ce point est assez amusant (et un peu particulier) car, généralement, Go, qui s'inspire de C et de Pascal grammaticalement parlant est moins dépaysant qu'Erlang qui s'inspire de Prolog et de Haskell.

Par contre, comme Go ne dépend pas d'une machine virtuelle, ce qui explique en grande partie le fait qu'il existe moins de mécanismes inhérents qui aident le développeur, le code machine produit pour des applications (de petite envergure) sera généralement très performant, plus qu'une application de petite envergure Erlang. Cependant, lorsque l'application est de très grande taille, l'organisation interne des modules de Erlang et sa répartition homogène sur tous les noeuds de calcul disponibles (CPU par exemple) rend généralement le code plus performant.

Chez Dernier Cri, nous pensons que :

  • Erlang est tout indiqué pour les personnes désirant apprendre la conception de systèmes concurrents ;
  • Go offre des mécanismes plus bas niveaux et est donc adapté pour les personnes désirant vérifier leurs compétences en implémentant tous les éléments concurrents à la main.

Pour les personnes plus expérimenté, les outils, la syntaxe et ce qui entoure le langage sera déterminant dans le choix d'une technologie pour un projet nécéssitant de la concurrence.

Dans le futur, nous dédierons sans aucun doute un article sur la programmation distribuée et nous verrons pourquoi, sur ce point, Erlang ne laisse aucune chance à Go !

D'ailleurs, il faudra un jour que j'écrive tout le mal (en toute amitiée) que je pense de Go, ce serait une forme de liste de souhaits pour la v2.0 du langage (actuellement en version 1.7).