11.4.3.2.4 : Synchronisation
Les garanties offertes par la cohérence de cache ne sont pas suffisantes pour la plupart des applications. Le fait qu'elles opèrent à une très petite granularité (accès mémoire de la taille d'un pointeur en général) signifie que~:
- Si deux threads écrivent sur une même structure de données en même temps, ils peuvent laisser en mémoire un mélange binaire des deux versions de la structure, généralement invalide pour l'application.
- Si un thread lit une structure de données qu'un autre thread est en train d'écrire, il peut observer un mélange de l'ancien et du nouveau contenu de la structure, lui aussi généralement invalide.
De plus, pour ne rien arranger, les CPUs et les compilateurs s'autorisent à transformer les accès mémoire des applications de diverses manières, par exemple en ajoutant ou supprimant des lectures / écritures, ou en les réordonnant. Ces optimisations, initialement mises en place car elles étaient invisibles pour le code séquentiel, rendent très difficile tout raisonnement basé sur les accès mémoire dans un code parallèle.
Enfin, la mémoire partagée n'est pas un outil adapté pour un thread souhaitant attendre qu'un autre ait terminé son travail. On ne peut alors utiliser que l'attente active, qui est un gaspillage de cycles CPU auquel aucun développeur soucieux de performances n'acceptera de se livrer pour de longues durées. Et les mécanismes matériels d'interruption inter-processeur permettant de faire mieux sont bien trop bas niveau et bien trop dangereux pour être exposés sans restriction à une application non privilégiée.
Les compilateurs de langages de programmation collaborent donc avec les systèmes d'exploitation pour offrir diverses réponses à ces problèmes. Par exemple, dans PThread, le mutex permet à des threads de se mettre d'accord pour accéder à une donnée à tour de rôle, chaque thread attendant que le précédent ait fini avant d'accéder à la donnée. Et la condition variable permet à un thread de s'endormir en attendant qu'un autre thread ne le réveille.
Mais bien qu'ils simplifient l'écriture de code multi-cœur, ces mécanismes ne sont pas faciles à utiliser. Il n'est que trop facile d'oublier de les employer, de les utiliser de façon incorrecte, d'en faire un usage excessif nuisible à la performance, ou d'en avoir des attentes irréalistes comme d'espérer que synchroniser les accès à deux variables séparément conduise les autres threads à garder de ces deux variables une vision synchrone.