Hmm… Ok, donc ce n’est pas le modèle en général qui est considéré comme mauvais, mais « seulement » la partie ajoutée par la norme C11 en rapport avec les threads.
C’est effectivement ça qu’on appelle le "modèle mémoire". Si tu pensais à la façon dont les données sont représentées, on pourrait parler de "représentation mémoire" par exemple ?
À ce sujet, tant que la discussion est orientée sur les threads, petite question plus « existentielle » : est-ce véritablement au langage de fournir les outils pour gérer le multi-threading ? Je veux dire, techniquement, il faudra toujours passer par l’OS pour créer et gérer des threads. Dès lors, la gestion des opérations en découlant (comme les opérations atomiques), ne devrait-elle pas revenir également à l’OS qui fournirait alors une API pour ces dernières opérations ? Sur ce point, la bibliothèque pthread ne fait-elle pas le boulot qu’essaye de réaliser (en moins bien ?) la norme C11 ?
Il faut passer par l’OS pour faire plein de trucs, ce n’est pas vraiment la question ici. Le problème est de donner un sens (une "sémantique", au sens théorique du terme, qui a l’air un peu différent de ce pour quoi les C++iens utilisent ce mot) à des programmes concurrents, notamment en présence de data race (deux processus qui peuvent accéder "simultanément" à une variable ).
Le modèle le plus fort est celui de la cohérence séquentielle : il stipule que les exécutions autorisées d’un programme correspondent aux entrelacements des instructions des différents threads. Le problème est que ce modèle n’autorise pas tout une classe d’optimisations des compilateurs. Par exemple, dans le programme suivant :
| /* Initialement, x = 0 && y = 0. r1 et r2 sont des registres locaux à chaque processus */
/* Thread 1 */ | /* Thread 2 */
x = 1; | y = 1;
r1 = y; | r2 = x
|
Le compilateur (on peut aussi voir ça comme "le processeur qui exécute le thread 1, et n’est pas au courant de ce que font les autres") peut décider pour une raison d’inverser les deux instructions du thread 1, qui sont indépendantes : il s’agit d’une écriture d’une variable, et d’une lecture d’une autre variable. Cette optimisation est correcte dans le cas d’un programme séquentiel, mais ici elle change la sémantique du programme complet : elle permet d’arriver en fin d’exécution à r1 == 0 && r2 == 0
, ce qui n’est pas permis par la cohérence séquentielle.
Une solution serait d’interdire les optimisations qui cassent ce modèle, mais c’est trop pénalisant pour les performances, donc on ne le fait pas. À une époque, le C se simplifiait la vie en disant simplement "en cas de data race, UB". Ici par exemple, tout était autorisé (puisque le thread 1 peut lire y "en même temps" que le thread 2 écrit dedans).
Depuis, il y a eu des progrès, et depuis C11 le C dispose d’un modèle mémoire un peu plus fin. Seulement il se trouve qu’il pose encore des problèmes, notamment parce qu’il autorise dans certains cas des optimisations qui cassent complètement la sémantique des programmes (des lectures out of thin air, c’est-à-dire qu’on peut lire dans une variable une valeur qui n’est présente à aucun endroit dans le code du programme).
On retrouve aussi le besoin de définir un modèle mémoire au niveau des processeurs eux-mêmes (historiquement, c’est d’ailleurs là que ça a commencé). Par exemple, dans x86 (qui est un des modèles les plus stricts, mais qui n’est pas non plus la cohérence séquentielle), le réordonnancement dont je parlais plus tôt est autorisé (et on peut donc avoir les deux registres à 0 à la fin de l’exécution du programme). POWER et ARM ont un modèle très relâché, et entre les deux il y a quelques architectures qui ont quelque chose d’intermédiaire (SPARC il me semble par exemple, mais je confonds peut-être).
Au niveau des langages, Java a aussi essayé de faire le même travail. Pour le coup je n’y connais pas grand chose, mais j’ai cru comprendre que c’était aussi bien cassé.