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 ?