11.4.3.2.6 : Paradigmes pour le parallélisme


Si la disponibilité grandissante d'abstractions haut niveau pour le parallélisme multi-cœur est une excellente chose, car elle évite que certains types de codes parallèle soient réécrits sans cesse en faisant des erreurs, cela n'est pas une raison pour négliger les fondamentaux de la programmation parallèle.

En effet, même avec tous ces outils, il arrive encore trop souvent de se retrouver face à une situation où l'on doit gérer le parallélisme soi-même, et se retrouve alors face au chaos décrit dans lasection 11.4.3.2.4. À tout instant, dans le parallélisme en mémoire partagée, on n'est qu'à une variable partagée de la catastrophe, et les erreurs peuvent ne pas être repérées pendant les tests si elles ne se manifestent que dans un ordre d'exécution bien précis.

Mais on peut faire mieux en restreignant volontairement le code que l'on écrit de façon à éviter l'existence d'état partagé et modifiable en mémoire, qui est la source de tous les maux, si possible avec l'assistance d'un langage de programmation conçu pour garantir le respect de cette restriction. Trois grandes stratégies peuvent ainsi être employées.

Dans une première stratégie, issue du monde de la programmation fonctionnelle, toutes les données qui sont partagées entre les threads sont en lecture seule. On distingue le style fonctionnel pur, où l'on s'interdit l'utilisation de données modifiables même lorsqu'elles ne sont pas partagées, du style impur où l'on s'autorise des modifications de données tant que celles-ci ne sont pas visibles du reste du programme.

L'utilisation d'un langage de programmation fonctionnel comme OCaml ou Haskell permet de faire vérifier le code par le compilateur pour s'assurer que ce style est bien suivi.

Dans une seconde stratégie, issue du monde des télécommunications, on décompose le programme en un grand nombre de sous-programmes (ne correspondant pas nécessairement à des threads du système d'exploitation) dont l'état interne est isolé et qui ne communiquent les uns les autres que par échange de messages. Selon que l'échange de message soit synchrone ou asynchrone, on parle de processus communiquants ou d'acteurs. Cette stratégie, qui se rapproche du calcul distribué, s'en distingue quand elle autorise l'existence d'état partagé en lecture seule commun, et la transmission de simples pointeurs vers un tel état partagé.

L'utilisation d'un langage de programmation conçu pour ce paradigme comme Go ou Erlang ne garantit malheureusement pas toujours une vérification parfaite du respect du paradigme (les garanties exactes dépendent du langage), mais augmente grandement son ergonomie et ses performances par l'introduction d'un mécanisme de processus légers moins coûteux que les threads.

Enfin, une troisième stratégie consiste à combiner ces deux approches. C'est ce que permet le langage Rust en vérifiant à la compilation qu'à chaque instant de l'exécution du programme une variable sera soit accessible en écriture soit partagée, tout en autorisant les variables à basculer d'un de ces états à l'autre au fil de l'exécution du programme.