web-dev-qa-db-fra.com

Comment puis-je comprendre les barrières de mémoire lues et volatiles

Certaines langues fournissent un modificateur volatile qui est décrit comme effectuant une "barrière de mémoire de lecture" avant de lire la mémoire qui sauvegarde une variable.

Une barrière de mémoire de lecture est généralement décrite comme un moyen de garantir que le CPU a effectué les lectures demandées avant la barrière avant d'effectuer une lecture demandée après la barrière. Cependant, en utilisant cette définition, il semblerait qu'une valeur périmée puisse toujours être lue. En d'autres termes, effectuer des lectures dans un certain ordre ne semble pas signifier que la mémoire principale ou d'autres processeurs doivent être consultés pour garantir que les valeurs suivantes lues reflètent réellement les dernières du système au moment de la barrière de lecture ou écrites ultérieurement après la lire la barrière.

Alors, volatile garantit-il vraiment qu'une valeur à jour est lue ou simplement (halètement!) Que les valeurs lues sont au moins aussi à jour que les lectures avant la barrière? Ou une autre interprétation? Quelles sont les implications pratiques de cette réponse?

55
Jason Kresowaty

Il existe des barrières de lecture et d'écriture; acquérir des barrières et libérer des barrières. Et plus (io vs mémoire, etc.).

Les barrières ne sont pas là pour contrôler la "dernière" valeur ou la "fraîcheur" des valeurs. Ils sont là pour contrôler l'ordre relatif des accès mémoire.

Les barrières d'écriture contrôlent l'ordre des écritures. Étant donné que les écritures en mémoire sont lentes (par rapport à la vitesse du processeur), il existe généralement une file d'attente de demandes d'écriture où les écritures sont publiées avant qu'elles ne se produisent réellement. Bien qu'ils soient mis en file d'attente dans l'ordre, les écritures peuvent être réorganisées à l'intérieur de la file d'attente. (Alors peut-être que "faire la queue" n'est pas le meilleur nom ...) À moins que vous n'utilisiez des barrières d'écriture pour empêcher la réorganisation.

Les barrières de lecture contrôlent l'ordre des lectures. En raison de l'exécution spéculative (le CPU regarde devant et se charge tôt dans la mémoire) et en raison de l'existence du tampon d'écriture (le CPU lira une valeur du tampon d'écriture au lieu de la mémoire si elle est là - c'est-à-dire que le CPU pense qu'il vient d'écrire X = 5, alors pourquoi le relire, il suffit de voir qu'il attend toujours de devenir 5 dans le tampon d'écriture) les lectures peuvent se produire dans le désordre.

Cela est vrai indépendamment de ce que le compilateur essaie de faire par rapport à l'ordre du code généré. c'est-à-dire que "volatile" en C++ n'aidera pas ici, car il dit seulement au compilateur de sortir du code pour relire la valeur de "mémoire", il ne dit PAS au CPU comment/où le lire (c'est-à-dire "mémoire" est beaucoup de choses au niveau du processeur).

Ainsi, les barrières de lecture/écriture mettent en place des blocs pour empêcher la réorganisation dans les files d'attente de lecture/écriture (la lecture n'est généralement pas tellement une file d'attente, mais les effets de réorganisation sont les mêmes).

Quels types de blocs? - acquérir et/ou libérer des blocs.

Acquérir - par exemple, lecture-acquisition (x) ajoutera la lecture de x dans la file d'attente de lecture et videra la file d'attente (pas vraiment vider la file d'attente, mais ajoutez un marqueur indiquant ne rien réorganiser avant cette lecture, ce qui revient à dire que la file d'attente a été vidée). Ainsi, les lectures ultérieures (dans l'ordre du code) peuvent être réorganisées, mais pas avant la lecture de x.

Release - par exemple, write-release (x, 5) videra (ou marquera) la file d'attente en premier, puis ajoutera la demande d'écriture à la file d'attente d'écriture. Ainsi, les écritures antérieures ne seront pas réorganisées pour se produire après x = 5, mais notez que les écritures ultérieures peuvent être réorganisées avant x = 5.

Notez que j'ai jumelé la lecture avec l'acquisition et l'écriture avec la version car cela est typique, mais différentes combinaisons sont possibles.

L'acquisition et la libération sont considérées comme des "demi-barrières" ou des "demi-clôtures" car elles empêchent uniquement le réordonnancement d'aller dans un sens.

Une barrière complète (ou clôture complète) applique à la fois une acquisition et une libération - c'est-à-dire aucune réorganisation.

Généralement pour la programmation sans verrouillage, ou C # ou Java 'volatile', ce que vous voulez/avez besoin est lecture-acquisition et écriture-libération.

c'est à dire

void threadA()
{
   foo->x = 10;
   foo->y = 11;
   foo->z = 12;
   write_release(foo->ready, true);
   bar = 13;
}
void threadB()
{
   w = some_global;
   ready = read_acquire(foo->ready);
   if (ready)
   {
      q = w * foo->x * foo->y * foo->z;
   }
   else
       calculate_pi();
}

Donc, tout d'abord, c'est une mauvaise façon de programmer les threads. Les verrous seraient plus sûrs. Mais juste pour illustrer les obstacles ...

Une fois que threadA () a fini d'écrire foo, il doit écrire foo-> ready LAST, vraiment dernier, sinon d'autres threads pourraient voir foo-> ready plus tôt et obtenir les mauvaises valeurs de x/y/z. Nous utilisons donc un write_release Sur foo-> ready, qui, comme mentionné ci-dessus, vide efficacement la file d'attente d'écriture (en s'assurant que x, y, z sont validés) puis ajoute la demande ready = true à la file d'attente. Et puis ajoute la barre = 13 demande. Notez que puisque nous venons d'utiliser une barrière de libération (pas une pleine), la barre = 13 peut être écrite avant d'être prête. Mais on s'en fout! c'est-à-dire que nous supposons que la barre ne modifie pas les données partagées.

Maintenant, threadB () doit savoir que lorsque nous disons "prêt", nous voulons vraiment dire prêt. Nous faisons donc une read_acquire(foo->ready). Cette lecture est ajoutée à la file d'attente de lecture, PUIS la file d'attente est vidée. Notez que w = some_global Peut également toujours être dans la file d'attente. Donc foo-> ready peut être lu avant some_global. Mais encore une fois, nous ne nous en soucions pas, car cela ne fait pas partie des données importantes pour lesquelles nous sommes si prudents. Ce qui nous importe, c'est foo-> x/y/z. Ils sont donc ajoutés à la file d'attente de lecture après l'acquisition flush/marker, garantissant qu'ils ne sont lus qu'après lecture de foo-> ready.

Notez également qu'il s'agit généralement des mêmes barrières que celles utilisées pour verrouiller et déverrouiller un mutex/CriticalSection/etc. (c.-à-d. acquérir sur lock (), relâcher sur unlock ()).

Alors,

  • Je suis presque sûr que cela (c'est-à-dire acquérir/libérer) est exactement ce que MS Docs dit se produit pour la lecture/écriture de variables "volatiles" en C # (et éventuellement pour MS C++, mais ce n'est pas standard). Voir http://msdn.Microsoft.com/en-us/library/aa645755 (VS.71) .aspx incluant "Une lecture volatile a" acquis la sémantique "; c'est-à-dire qu'elle est garantie de se produire avant toute référence à la mémoire qui se produit après elle ... "

  • Je pense Java est le même, bien que je ne sois pas aussi familier. Je soupçonne que c'est exactement la même chose, car vous n'avez généralement pas besoin de plus de garanties que la lecture-acquisition/libération-écriture.

  • Dans votre question, vous étiez sur la bonne voie en pensant que tout dépend de l'ordre relatif - vous venez d'avoir les ordres en arrière (c'est-à-dire "les valeurs qui sont lues sont au moins aussi à jour que les lectures avant la barrière?" "- non, les lectures avant la barrière sont sans importance, ses lectures APRÈS la barrière qui sont garanties d'être APRÈS, et vice versa pour les écritures).

  • Et s'il vous plaît noter, comme mentionné, la réorganisation se produit à la fois en lecture et en écriture, donc utiliser uniquement une barrière sur un thread et non sur l'autre NE FONCTIONNERA PAS. c'est-à-dire qu'une libération en écriture ne suffit pas sans lecture-acquisition. c'est-à-dire que même si vous l'écrivez dans le bon ordre, il pourrait être lu dans le mauvais ordre si vous n'utilisiez pas les barrières de lecture pour aller avec les barrières d'écriture.

  • Et enfin, notez que la programmation sans verrouillage et les architectures de mémoire CPU peuvent être en réalité beaucoup plus compliquées que cela, mais s'en tenir à acquérir/libérer vous mènera assez loin.

111
tony

volatile dans la plupart des langages de programmation n'implique pas une véritable barrière de mémoire de lecture CPU mais un ordre au compilateur de ne pas optimiser les lectures via la mise en cache dans un registre. Cela signifie que le processus de lecture/thread obtiendra la valeur "éventuellement". Une technique courante consiste à déclarer un drapeau booléen volatile à définir dans un gestionnaire de signaux et à vérifier dans la boucle de programme principale.

En revanche, les barrières de mémoire du processeur sont fournies directement via les instructions du processeur ou impliquées avec certains mnémoniques d'assembleur (tels que le préfixe lock en x86) et sont utilisées par exemple lors de conversations avec des périphériques matériels où l'ordre de lit et écrit dans les registres IO mappés en mémoire) est important ou synchronise l'accès à la mémoire dans un environnement multi-traitement.

Pour répondre à votre question - non, la barrière de mémoire ne garantit pas la "dernière" valeur, mais garantit ordre des opérations d'accès à la mémoire. Ceci est crucial par exemple dans la programmation sans verrouillage .

ici est l'une des amorces sur les barrières de mémoire CPU.

9
Nikolai Fetissov