Immuable
En termes simples, la mémoire est immuable lorsqu'elle n'est pas modifiée après son initialisation.
Les programmes écrits dans des langages impératifs tels que C, Java et C# peuvent manipuler les données en mémoire à leur guise. Une zone de mémoire physique, une fois réservée, peut être modifiée en tout ou en partie par un thread d'exécution à tout moment pendant l'exécution du programme. En fait, les langages impératifs encouragent cette façon de programmer.
Écrire des programmes de cette manière a été incroyablement réussi pour les applications à thread unique. Cependant, à mesure que le développement d'applications modernes se dirige vers plusieurs threads d'opération concurrents au sein d'un même processus, un monde de problèmes potentiels et de complexité est introduit.
Lorsqu'il n'y a qu'un seul thread d'exécution, vous pouvez imaginer que ce thread unique "possède" toutes les données en mémoire et peut donc les manipuler à sa guise. Cependant, il n'y a pas de concept implicite de propriété lorsque plusieurs threads d'exécution sont impliqués.
À la place, ce fardeau incombe au programmeur qui doit faire de grands efforts pour garantir que les structures en mémoire sont dans un état cohérent pour tous les lecteurs. Les structures de verrouillage doivent être utilisées avec précaution pour empêcher qu'un thread ne voie des données pendant qu'elles sont mises à jour par un autre thread. Sans cette coordination, un thread consommerait inévitablement des données qui étaient seulement à moitié en train d'être mises à jour. Le résultat d'une telle situation est imprévisible et souvent catastrophique. De plus, faire fonctionner correctement les verrous dans le code est notoirement difficile et, quand c'est mal fait, peut paralyser les performances ou, dans le pire des cas, entraîner des impasses qui interrompent irrémédiablement l'exécution.
L'utilisation de structures de données immuables allège le besoin d'introduire des verrouillages complexes dans le code. Lorsqu'une section de la mémoire est garantie de ne pas changer pendant la durée de vie d'un programme, plusieurs lecteurs peuvent accéder simultanément à la mémoire. Il leur est impossible d'observer ces données particulières dans un état incohérent.
De nombreux langages de programmation fonctionnelle, tels que Lisp, Haskell, Erlang, F# et Clojure, encouragent les structures de données immuables par leur nature même. C'est pour cette raison qu'ils suscitent un regain d'intérêt alors que nous nous dirigeons vers un développement d'applications de plus en plus complexes à plusieurs threads et des architectures informatiques de plus en plus nombreuses.
État
L'état d'une application peut simplement être considéré comme le contenu de toute la mémoire et des registres de CPU à un moment donné.
Logiquement, l'état d'un programme peut être divisé en deux :
- L'état du tas
- L'état de la pile de chaque thread en cours d'exécution
Dans les environnements gérés tels que C# et Java, un thread ne peut pas accéder à la mémoire d'un autre. Par conséquent, chaque thread "possède" l'état de sa pile. La pile peut être considérée comme contenant des variables locales et des paramètres de type valeur (struct
), et les références aux objets. Ces valeurs sont isolées des threads externes.
Cependant, les données sur le tas peuvent être partagées entre tous les threads, donc il faut faire attention pour contrôler l'accès concurrent. Toutes les instances d'objets de type référence (class
) sont stockées sur le tas.
En POO, l'état d'une instance d'une classe est déterminé par ses champs. Ces champs sont stockés sur le tas et sont donc accessibles par tous les threads. Si une classe définit des méthodes permettant de modifier des champs après l'achèvement du constructeur, alors la classe est mutable (non immuable). Si les champs ne peuvent pas être modifiés de quelque manière que ce soit, alors le type est immuable. Il est important de noter qu'une classe avec tous les champs C# readonly
/Java final
n'est pas nécessairement immuable. Ces constructions garantissent que la référence ne peut pas changer, mais pas l'objet référencé. Par exemple, un champ peut avoir une référence immuable à une liste d'objets, mais le contenu réel de la liste peut être modifié à tout moment.
En définissant un type comme étant vraiment immuable, son état peut être considéré comme figé et donc le type est sûr d'être accédé par plusieurs threads.
En pratique, il peut être inconfortable de définir tous vos types comme immuables. Modifier une valeur sur un type immuable peut impliquer une bonne quantité de copie mémoire. Certains langages rendent ce processus plus facile que d'autres, mais de toute façon, le CPU finira par faire un travail supplémentaire. De nombreux facteurs contribuent à déterminer si le temps passé à copier la mémoire l'emporte sur l'impact des contensions de verrouillage.
Beaucoup de recherches ont été menées sur le développement de structures de données immuables telles que des listes et des arbres. Lors de l'utilisation de telles structures, par exemple une liste, l'opération 'ajout' renverra une référence à une nouvelle liste avec l'élément ajouté. Les références à la liste précédente ne voient aucun changement et ont toujours une vue cohérente des données.