10 votes

Est-ce que mutex_unlock fonctionne comme une barrière de mémoire ?

La situation que je vais décrire se produit sur un iPad 4 (ARMv7s), en utilisant les librairies posix pour verrouiller/déverrouiller les mutex. J'ai vu des choses similaires sur d'autres appareils ARMv7, cependant (voir ci-dessous), donc je suppose que toute solution nécessitera un regard plus général sur le comportement des mutex et des barrières mémoire pour ARMv7.

Pseudo code pour le scénario :

Fil conducteur 1 - Produire des données :

void ProduceFunction() {
  MutexLock();
  int TempProducerIndex = mSharedProducerIndex; // Take a copy of the int member variable for Producers Index
  mSharedArray[TempProducerIndex++] = NewData; // Copy new Data into array at Temp Index 
  mSharedProducerIndex = TempProducerIndex; // Signal consumer data is ready by assigning new Producer Index to shared variable
  MutexUnlock();
}

Fil 2 - Consommer des données :

void ConsumingFunction () {
  while (mConsumerIndex != mSharedProducerIndex) {
    doWorkOnData (mSharedArray[mConsumerIndex++]);
  }
}

Auparavant (lorsque le problème est apparu sur l'iPad 2), je pensais que mSharedProducerIndex = TempProducerIndex n'était pas exécuté de manière atomique, et donc modifié pour utiliser un filtre de type AtomicCompareAndSwap d'attribuer mSharedProducerIndex . Cela a fonctionné jusqu'à présent, mais il s'avère que j'avais tort et que le bogue est revenu. Je suppose que le "correctif" a juste changé le timing.

J'en suis maintenant arrivé à la conclusion que le problème réel est une exécution dans le désordre des écritures dans le verrou mutex, c'est-à-dire si le compilateur ou le matériel a décidé de réordonner :

mSharedArray[TempProducerIndex++] = NewData; // Copy new Data into array at Temp Index 
mSharedProducerIndex = TempProducerIndex;  // Signal consumer data is ready by assigning new Producer Index to shared variable

... à :

mSharedProducerIndex = TempProducerIndex; // Signal consumer data is ready by assigning new Producer Index to shared variable
mSharedArray[TempProducerIndex++] = NewData; // Copy new Data into array at Temp Index 

... puis le consommateur a intercalé le producteur, les données n'auraient pas encore été écrites lorsque le consommateur a essayé de les lire.

Après avoir lu quelques articles sur les barrières de mémoire, j'ai donc pensé essayer de déplacer le signal vers le consommateur en dehors de l'espace de stockage. mutex_unlock croyant que le déverrouillage produirait une barrière/clôture de mémoire qui garantirait mSharedArray avait été écrit :

mSharedArray[TempProducerIndex++] = NewData;  // Copy new Data into array at Temp Index 
MutexUnlock();
mSharedProducerIndex = TempProducerIndex; // Signal consumer data is ready by assigning new Producer Index to shared variable

Cependant, cela n'aboutit toujours pas, et me conduit à me demander si une mutex_unlock agira-t-il définitivement comme une barrière d'écriture ou non ?

J'ai aussi lu un article de HP qui suggérait que les compilateurs pouvaient déplacer le code dans (mais pas hors de) crit_sec s. Ainsi, même après la modification ci-dessus, l'écriture de mSharedProducerIndex pourrait être avant la barrière. Cette théorie a-t-elle un sens ?

En ajoutant une clôture explicite, le problème disparaît :

mSharedArray[TempProducerIndex++] = NewData; // Copy new Data into array at Temp Index 
OSMemoryBarrier();
mSharedProducerIndex = TempProducerIndex; // Signal consumer data is ready by assigning new Producer Index to shared variable

Je pense donc comprendre le problème, et qu'une barrière est nécessaire, mais tout aperçu du comportement du déverrouillage et de la raison pour laquelle il ne semble pas effectuer une barrière serait vraiment utile.

EDIT :

Concernant l'absence de mutex dans le thread du consommateur : Je me fie à l'écriture de la fonction int mSharedProducerIndex étant une instruction unique et espérant donc que le consommateur lira soit la nouvelle soit l'ancienne valeur. Les deux sont des états valides, et à condition que mSharedArray est écrit en séquence (c'est-à-dire avant d'écrire mSharedProducerIndex ), ce serait bien, mais d'après ce qui a été dit jusqu'à présent, je ne peux pas répondre à cette question.

Selon la même logique, il semble que la solution actuelle de la barrière soit également défectueuse, car la mSharedProducerIndex l'écriture pourrait être déplacée à l'intérieur de la barrière et pourrait donc potentiellement être réorganisée de manière incorrecte.

Est-il recommandé d'ajouter un mutex au consommateur, juste pour servir de barrière à la lecture, ou existe-t-il une autre solution ? pragma ou une instruction pour désactiver l'exécution hors ordre sur le producteur, comme EIEIO sur le CPP ?

7voto

auselen Points 13961

Vos produits sont synchronisés mais vous n'effectuez aucune synchronisation (vous devez également synchroniser la mémoire avec les barrières) lors de la consommation. Donc même si vous avez des barrières de mémoire parfaites pour les producteurs, ces barrières de mémoire n'aideront pas les consommateurs.

Dans votre code, vous pouvez être touché par l'ordre du compilateur, l'ordre du matériel, même par une valeur périmée de mSharedProducerIndex sur l'autre noyau qui exécute le fil n°2.

Vous devriez lire Chapter 11: Memory Ordering de Guide du programmeur de la série Cortex™-A notamment 11.2.1 Memory barrier use example .

Je pense que votre problème est que vous obtenez des mises à jour partielles dans le thread du consommateur. Le problème est que ce qui se trouve dans la section critique du producteur n'est pas atomique et peut être réorganisé.

Par not atomic Je veux dire que si votre mSharedArray[TempProducerIndex++] = NewData; n'est pas un magasin de mots (NewData a le type int), il peut être fait en plusieurs étapes qui peuvent être vues par d'autres noyaux comme des mises à jour partielles.

Par reordering Je veux dire que le mutex fournit des barrières d'entrée et de sortie mais n'impose aucun ordre pendant la section critique. Puisque vous n'avez pas de construction spéciale du côté du consommateur, vous pouvez voir que mSharedProducerIndex est mis à jour mais on voit toujours des mises à jour partielles de mSharedArray[mConsumerIndex] . Les mutex ne garantissent la visibilité de la mémoire que lorsque l'exécution quitte la section critique.

Je crois que cela explique également pourquoi cela fonctionne lorsque vous ajoutez OSMemoryBarrier(); à l'intérieur de la section critique, parce que de cette façon le processeur est forcé d'écrire des données dans mSharedArray puis actualiser mConsumerIndex et quand l'autre noyau/thread voit mConsumerIndex nous savons que mSharedArray est copié entièrement à cause de la barrière.

Je pense que votre mise en œuvre avec OSMemoryBarrier(); est correct en supposant que vous avez plusieurs producteurs et un consommateur. Je ne suis pas d'accord avec les commentaires suggérant de mettre une barrière mémoire dans le consommateur, car je pense que cela ne résoudra pas les mises à jour partielles ou les réorganisations qui se produisent dans la section critique du producteur.

Pour répondre à votre question dans le titre, en général, afaik mutex es ont une barrière de lecture avant d'entrer et une barrière d'écriture après leur départ.

6voto

Steve Jessop Points 166970

La "théorie" est correcte, les écritures peuvent être déplacées d'après une clôture d'écriture à avant celle-ci.

Le problème fondamental de votre code est qu'il n'y a pas de synchronisation du tout dans le fil 2. Vous lisez mSharedProducerIndex sans barrière de lecture, donc qui sait quelle valeur vous obtiendrez. Rien de ce que vous faites dans le fil 1 ne résoudra cela.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X