URL: https://linuxfr.org/news/instantane-sur-le-parallelisme-et-le-code Title: Instantané sur le parallélisme et le code Authors: Jiehong palm123, BAud, Benoît Sibaud, ZeroHeure et PolePosition Date: 2015年06月01日T22:06:09+02:00 License: CC By-SA Tags: thread Score: 54 Avant, la fréquence des micro-processeurs [semblait suivre une loi de Moore](https://fr.wikipedia.org/wiki/Loi_de_Moore#Loi_de_Moore_et_fr.C3.A9quence_d.27horloge), mais à mesure que les limites physiques ont été approchées il a fallu innover, et le multi-cœur est devenu le standard, même sur mobile. Développer une application qui sache utiliser plusieurs fils d’exécution ou processus en même temps n'est pas un problème forcément facile, pour lequel de nombreuses méthodes ont vu le jour. Ici, on vous propose une vision haut niveau de quelques idées du passé, mais surtout d'aujourd'hui, pour utiliser la puissance de nos machines sans écrire des bogues plus vite que nos ombres. ---- ---- # Exposé du problème Imaginez que vous deviez écrire et lire un même fichier dans deux processus différents. C'est notre cas simple de départ. ## Le cas trivial Dans ce cas, vos processus ne tournent jamais en même temps, ou alors aucun n'écrit quoi que ce soit sur le fichier et se contente de lire le fichier. Tout devrait marcher correctement. ## Le cas partagé Vos deux processus se mettent à tourner en même temps et ils ont carte blanche pour écrire ou supprimer le fichier. Il se peut que l'un des processus vérifie l'existence du fichier avant d'y écrire quelque chose, mais que l'autre processus l'efface juste après. Résultat : le premier processus va planter ou retourner une exception. Pourtant, le code aura l'air juste. # Différence entre *concurrence* et *parallélisme* La *concurrence* est d'abord le fait de partager un bout de la mémoire avec plusieurs processus (ou fils d'exécutions). Le *parallélisme* consiste en l'exécution de plusieurs processus en même temps, sur des données disjointes. C'est une distinction qui est plus forte en anglais, puisque en français le *parallélisme* n'est qu'un type de concurrence. Mais la séparation des deux est pratique d'un point de vue pédagogique. # Les verrous et sémaphores La première idée que l'on apprend consiste à mettre un cadenas sur l'espace mémoire partagé, avec une seule et même clé. C'est ce que l'on appelle un verrou. Les sémaphores ne sont que le cas où l'on fournit un nombre fini de clés supérieur à 1. En pseudo-code, ça ressemble souvent à ça : ``` python tant que verrou == 1: attend() verrou = 1 ## Je calcule ce dont j'ai besoin ici ## Et c'est assez long ## Puis je rend le verrou verrou = 0 ``` Avec deux processus, ça peut aller, mais quand le nombre augmente, il est très facile d'oublier de redonner la clé, et il est très facile de se retrouver avec des processus qui ne font qu'attendre une clé qui n'arrive jamais. Le nombre de cas différents à considérer [augmente de manière factorielle avec le nombre de bouts de mémoires partagés](https://fr.wikipedia.org/wiki/Coefficient_binomial). La quasi-totalité des langages de programmation vous laisseront faire ça. Le nombre élevé d'erreurs engendrées par cette technique a conduit à l'élaboration d'abstractions ou de manières de faire différentes. # Le modèle à acteurs (et objets actifs) [Dans les années 1970, une autre idée est apparue](http://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-194.pdf) : au lieu de partager des zones mémoires et de bloquer de manière difficilement prévisible, pourquoi ne pas supposer qu'un processus est ce que l'on appelle un *acteur*, et qu'il ne peut communiquer avec d'autres acteurs que via des messages. Chaque acteur effectue une action demandée par le biais de messages. Erlang est le plus grand représentant de cette manière de faire. L'avantage est que la manière d'envoyer les messages permet de facilement parler à des processus sur le réseau, sans avoir rien de spécial à faire. L'autre avantage, c'est la possibilité de pouvoir déboguer beaucoup plus facilement, même s'il est toujours possible que le système se bloque si tout le monde se met à attendre. Enfin, si un acteur se plante, il peut simplement être relancé et n'impacte pas les autres acteurs. En pseudo-code, ça se passerait à peu près comme ça : ``` python # calcul_x étant une fonction quelconque, attendant 2 paramètres. mon_acteur = nouvel_acteur() resultat = envoyer_a(mon_acteur, calcul_x, parametre_1, parametre_2) # # Il est à noter que `resultat` n'est pas forcément calculé de manière synchrone # puisque `mon_acteur` fait peut-être autre chose, mais cette nouvelle demande est dans sa liste (ou boîte mél) ``` Les [*objets actifs*](https://en.wikipedia.org/wiki/Active_object) ne sont que de simples instances de classes, considérés comme des acteurs, ce qui s'insère très simplement dans la programmation objet : l'appel à une méthode se fait comme d'habitude, mais elle est transformée en message passé au-dit acteur à l’exécution, par exemple). À noter : [*Message Passing Interface*](https://fr.wikipedia.org/wiki/Message_Passing_Interface) (MPI), utilisé principalement au sein des supercalculateurs est un modèle de communication entre machines et leurs processus. Le modèle à acteur est transparent (c'est-à-dire qu'un acteur peut être sur la machine locale ou sur une autre machine). D'ailleurs, MPI peut être utilisé pour implémenter le modèle à acteur. Le modèle à acteur n'est qu'un exemple de la manière de penser de tout un pan de recherche basé sur l'[algèbre de processus](https://fr.wikipedia.org/wiki/Alg%C3%A8bre_de_processus) qui a le vent en poupe puisque c'est quelque chose de calqué sur des processus naturels dont on a l'habitude (comme les communications intercellulaires au sein d'un organisme). Je vous conseille d'essayer ça avec Erlang, [Akka](http://akka.io/) ou [Quasar](https://github.com/puniverse/quasar) pour Java/Scala et [Celluloid](https://github.com/celluloid/celluloid/) pour Ruby. Il est également possible de faire ça en Python grâce à [Pykka](https://pykka.readthedocs.org/en/latest/index.html) par exemple (ou [pulsar](https://pythonhosted.org/pulsar/)), [SObjectizer](http://sourceforge.net/projects/sobjectizer/) ou [CAF_C++ Actor Framework](http://actor-framework.org/) pour le C++, [hactor](https://hackage.haskell.org/package/hactor) pour Haskell. # Mémoire transactionnelle logicielle (*Software Transactional Memory* — STM) Une autre idée est de se baser sur ce que font les bases de données, notamment en assurant l'*atomicité* des transactions : c'est l'idée de la mémoire transactionnelle ([dans les années 80, sous le nom de Paratran](http://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-454.pdf)) C'est-à-dire que la transaction commence lorsque vous ouvrez le fichier, et se termine quand vous avez fini d'écrire. Rien ne peut se passer sur le fichier avant que vous n'ayez fini. C'est une manière de cacher les *mutex*, mais c'est souvent implémenté *sans* verrous. De manière générale, cette technique est implémentée de manière *optimiste*, notamment dans Haskell, avec un type de variable spéciale : la `TVar`. L'idée étant que chaque processus utilisant une TVar le fait comme si ne rien n'était, à partir de la valeur de la TVar telle qu'elle était à la vérification. Puis, chaque modification de cette TVar est enregistrée dans un *journal* propre à chaque processus. En pseudo code, voici à quoi ça pourrait ressembler : ``` python bloc atomique: valeur = lire_TVAR(x) # on fait plein de chose, et la valeur change ecrire_TVAR(x, valeur) #fin bloc atomique ``` À la fin du bloc atomique, la valeur de la TVar est lue et comparée à celle lue en début du bloc. Si cette valeur est identique, alors personne n'a modifié la TVar, et la nouvelle valeur est écrite de manière sûre. Dans le cas contraire, le bloc atomique est rejoué avec la nouvelle valeur de la TVar, d'où le nom *optimiste*. En Haskell, il est possible d'attendre une certaine valeur pour une TVar via un *if* et le « mot clé » `retry` : un signal est lié à la TVar, et sa valeur n'est re-vérifiée qu'après avoir été changée (programmation évènementielle). Tout ceci étant transparent pour le développeur. En pseudo-code, ça donne quelque chose comme ça : ``` python bloc atomique: valeur = lire_TVAR(x) si valeur> 0: retry # tout le bloc atomique sera ré-exécuté quand `valeur` aura changé. Ce qui bloque le processus. ecrire_TVAR(x, nouvelle_valeur) # fin bloc atomique ``` Je vous conseille d'essayer ça en Haskell avec [STM](https://hackage.haskell.org/package/stm), en Java avec [DeuceSTM](https://github.com/DeuceSTM/DeuceSTM) ou avec Clojure. C'est également possible en C++ avec [TinySTM](http://www.tmware.org/tinystm), . Par contre, en Python, il faudra repasser. # Voilà Vous avez pu entrevoir quelques techniques permettant de gérer la concurrence d'une manière moins *traditionnelle*, et vous êtes libres d'essayer tout ça si ça vous a plu. Les commentaires sont également là pour préciser des choses, ou discuter d'autres systèmes qui ne sont pas mentionnés dans cette dépêche.