informatique (MP2I/MPI) · Chapitre 22 · Application directe du cours
volatile n'est pas une synchronisation
Concurrence et synchronisation · niveau 2
Énoncé
On entend souvent qu'il suffit de déclarer une variable partagée volatile. Le mettre à l'épreuve : quatre fils, incréments chacun.
Corrigé
Mesuré, sur quatre fils et incréments attendus :
| déclaration | essai 1 | essai 2 | essai 3 |
|---|---|---|---|
int compteur | |||
volatile int compteur |
Aucune différence. Le volatile n'a rien corrigé, et les deux colonnes sont indiscernables.
Ce que volatile dit vraiment : « ne garde pas cette variable dans un registre, relis-la en mémoire à chaque accès ». C'est une contrainte sur les optimisations du compilateur, faite pour les registres matériels dont la valeur change toute seule. Elle ne dit rien sur l'atomicité.
Or le défaut n'est pas là. compteur = compteur + 1 reste lire, ajouter, écrire : trois opérations, avec ou sans volatile. Un autre fil peut toujours s'intercaler entre la lecture et l'écriture, et l'incrément se perd exactement comme avant.
On pourrait croire que _Atomic int règle tout. Mesuré :
sur un _Atomic int a | obtenu |
|---|---|
a = a + 1; | |
atomic_fetch_add de |
La première ligne est toujours fausse, et c'est instructif. Le type atomique rend atomique chaque lecture et chaque écriture — mais a = a + 1 en fait deux : une lecture atomique, puis une écriture atomique, avec un intervalle entre les deux. Seule une opération de lecture-modification-écriture indivisible, comme atomic_fetch_add, résout le problème.
La conclusion, et c'est la seule à retenir du chapitre : l'atomicité ne se déclare pas sur une variable, elle s'obtient sur une opération. Les deux moyens au programme sont le mutex et le sémaphore, et ils encadrent un morceau de code — pas une donnée. Tout le reste est une croyance.