11
votes

Comment les systèmes filetés font-ils face à des données partagées en cours de mise en cache par différents processeurs?

Je viens en grande partie d'un contexte C ++, mais je pense que cette question s'applique à la filetage dans n'importe quelle langue. Voici le scénario:

  1. Nous avons deux threads (fila et threadb) et une valeur x dans la mémoire partagée

  2. suppose que l'accès à X est contrôlé de manière appropriée par un mutex (ou une autre commande de synchronisation appropriée)

  3. Si les threads arrivent à exécuter sur différents processeurs, que se passe-t-il si Threada effectue une opération d'écriture, mais son processeur place le résultat dans son cache L2 plutôt que la mémoire principale? Ensuite, si THIPB tente de lire la valeur, il ne suffira-t-il pas simplement dans son propre cache L1 / L2 / mémoire principale, puis travaillez avec quelle que soit la valeur ancienne?

    Si ce n'est pas le cas, alors comment ce problème est-il géré?

    Si tel est le cas, alors que peut-on faire à ce sujet?


0 commentaires

4 Réponses :


12
votes

Votre exemple fonctionnerait bien.

Plusieurs processeurs utilisent un protocole de cohérence tel que Mesi pour que les données restent synchronisées entre les caches. Avec Mesi, chaque ligne de cache est considérée comme modifiée, exclusivement détenue, partagée entre CPU ou invalide. Écrire une ligne de cache partagée entre les processeurs le force à devenir invalide dans l'autre CPU, en gardant les caches en synchronisation.

Cependant, cela ne suffit pas assez. Différents processeurs ont différents Modèles de mémoire et la plupart des processeurs modernes soutiennent un certain niveau de réchérir des accès à la mémoire. Dans ces cas, barrières de mémoire sont nécessaires.

Par exemple si vous avez Filetage A: xxx

et thread b: xxx

avec les deux opérateurs distincts, il n'y a aucune garantie que le Les écritures effectuées à la dowork () seront visibles pour filer B avant que l'écriture sur Workdone et Dosomethresults () procéderait à un état potentiellement incompatible. Les barrières de mémoire garantissent une commande des lectures et écrit - ajout d'une barrière de mémoire après que Dowork () dans le fil a forcerait toutes les lectures / écritures effectuées par Dowork à compléter avant l'écriture sur Workdone, de sorte que le fil B obtiendrait une vue cohérente. Les mutiles fournissent de manière intrinsèquement une barrière de mémoire, de sorte que la lecture / écrit ne peut pas passer un appel à verrouiller et déverrouiller.

Dans votre cas, un processeur signalerait aux autres que cela a salué une ligne de cache et forcer les autres processeurs recharger de la mémoire. Acquérir le mutex à lire et écrire la valeur garantit que la modification de la mémoire est visible à l'autre processeur de l'ordre attendu.


1 commentaires

Merci beaucoup pour cette réponse. Je m'étais demandé si une sorte de mécanisme de niveau du matériel doit entrer en jeu ici, car il semblait qu'il y avait des limites pratiques sur ce qui pourrait être accompli au niveau de la langue / du compilateur.



2
votes

La plupart des primitives de verrouillage comme des mutexs impliquent barrières de mémoire . Celles-ci forcent une cache affleurant et rechargées à se produire.

Par exemple, P>

ThreadA {
    x = 5;         // probably writes to cache
    unlock mutex;  // forcibly writes local CPU cache to global memory
}
ThreadB {
    lock mutex;    // discards data in local cache
    y = x;         // x must read from global memory
}


6 commentaires

Je ne crois pas que les barrières forcent une cache de rinçage, elles forcent des contraintes sur l'ordre des opérations de mémoire. Une cache Flush ne vous aidera pas si l'écriture sur X peut passer le déverrouillage du mutex.


Les barrières seraient assez inutiles si le compilateur reconverse des opérations de mémoire à travers eux, HMM? Au moins pour GCC, je pense que cela est généralement mis en œuvre avec une mémoire de mémoire, qui indique à GCC "invalider toutes les hypothèses sur la mémoire".


Oh, je vois ce que tu dis. Une rinçage de cache n'est pas nécessaire, tant que la commande est correctement respectée entre les processeurs. Je suppose donc que cette explication est une vue simplifiée et la vôtre est davantage sur les détails matériels.


En ce qui concerne le programmeur, cependant, il n'y a pas de différence pratique (en termes d'exactitude) entre «Ce bit de mémoire doit être extrait à partir du cache de ce processeur" et "l'état mondial de Ce bit de mémoire est maintenant défini ".


Droit. Je ne pense pas que vous voudriez que vous voudriez une cache rincer ici, pour l'affaire commune espérante où la mémoire n'est en réalité pas partagée entre les transformateurs. Sur X86, une barrière mémoire est généralement juste une instruction de verrouillage XCHG, que je ne crois pas n'a aucun effet sur le cache.


FWIW, pour un simple verrouillage de spin, vous pouvez verrouiller Peformer en tant qu'atomic-Xchg pour définir un drapeau de propriété suivi d'une barrière mémoire en lecture (Aquire). Déverrouiller peut être une barrière d'écriture (version) suivie d'une écriture non atomique pour effacer le drapeau de propriété. En aucun cas, un cache-flush requis. Sur X86, où les barrières de mémoire sont implicites, vous n'avez besoin que d'une seule opération atomique.



0
votes

En général, le compilateur comprend la mémoire partagée et prend des efforts considérables pour assurer que la mémoire partagée est placée dans un endroit satable. Les compilateurs modernes sont très compliqués dans la manière dont ils commandent des opérations et des accès à la mémoire; Ils ont tendance à comprendre la nature du filetage et de la mémoire partagée. Cela ne veut pas dire qu'ils sont parfaits, mais en général, une grande partie de la préoccupation est prise en charge par le compilateur.


0 commentaires

0
votes

c # a une certaine construction de soutien à ce type de problèmes. Vous pouvez marquer une variable avec le mot clé code> code> code>, qui force à être synchronisé sur tous les CPU.

public list users;
// In some function:
System.Threading.Monitor.Enter(users);
try {
   // do something with users
}
finally {
   System.Threading.Monitor.Exit(users);
}


0 commentaires