J'ai trouvé un exemple de condition de race que j'ai pu reproduire sous g ++ code> sous Linux. Ce que je ne comprends pas, c'est comment l'ordre des opérations comporte dans cet exemple.
int va = 0;
void fa() {
for (int i = 0; i < 10000; ++i)
++va;
}
void fb() {
for (int i = 0; i < 10000; ++i)
--va;
}
int main() {
std::thread a(fa);
std::thread b(fb);
a.join();
b.join();
std::cout << va;
}
3 Réponses :
La norme dit (citant le dernier brouillon): P>
[intro.Races] p>
Deux évaluations d'expression conflit si l'un d'entre eux modifie un emplacement de mémoire ([intro.memory]) et l'autre lit ou modifie le même emplacement de mémoire. P>
L'exécution d'un programme contient une course de données s'il contient deux actions contradictoires potentiellement simultanément simultanées, au moins une d'entre elles n'est pas atomique et qui ne se produit pas non plus avant l'autre, à l'exception du cas spécial des gestionnaires de signaux décrits ci-dessous. Une telle course de données entraîne un comportement
indéfini fort>. P> blockQuote> Votre exemple de programme a une course de données et le comportement du programme est indéfini. P>
Ce que je ne comprends pas, c'est comment l'ordre des opérations comporte dans cet exemple. p> blockQuote>
L'ordre des opérations compte parce que les opérations ne sont pas atomiques et qu'ils lisent et modifient le même emplacement de mémoire. P>
Peut entretenir que la commande compte si j'avais utilisé VA = VA + 1; Parce que alors RHS VA aurait pu changer avant de revenir à la LHS VA P> attribuée blockQuote>
La même chose s'applique à l'opérateur d'incrément. La machine abstraite: p>
- Lire une valeur de la mémoire li>
- incrément la valeur li>
- Écrivez une valeur à la mémoire li> ul>
Il y a plusieurs étapes là-bas qui peuvent interlaisser avec des opérations dans l'autre thread. P>
Même s'il y avait une seule opération par fil, il n'y aurait aucune garantie de comportement bien défini à moins que ces opérations soient atomiques. P>
Remarque en dehors de la portée de C ++: une CPU peut avoir une seule instruction pour incrémenter un entier en mémoire. Par exemple, X86 a une telle instruction. Il peut être invoqué à la fois atomiquement et non atomique. Il serait inutile que le compilateur utilise l'instruction atomique, à moins que vous n'utilisiez explicitement des opérations atomiques en C ++. P>
L'idée importante ici est que lorsque C ++ est compilé, il est "traduit" en langage de montage. La traduction de voir ici pour une explication de ce que le code de montage pour ces instructions ressemblera. P > Afin de faire des opérations atomiques, la variable pourrait utiliser un mécanisme de verrouillage. Vous pouvez le faire en déclarant une variable atomique (qui gérera la synchronisation des threads pour vous): p> référence: https://fr.cppreference.com/w/cpp/atomic/atomic p> p> ++ va code> ou
- VA code> entraînera un code d'assemblage qui déplace la valeur de
VA code> à un registre, puis stocke le résultat d'ajouter 1 à ce registre Retour à
VA code> dans une instruction distincte. De cette manière, c'est exactement la même chose que
VA = VA + 1; code>. Cela signifie également que l'opération
VA ++ code> n'est pas nécessairement
IMHO, votre première partie est légèrement trompeuse. Ub peut entraîner un assemblage qui a l'air bien un jour mais pas l'autre jour
@ AncienlyknownS_463035818 J'ai essayé de rendre cela plus clairement en soulignant que l'opération C ++ est non atomique, car les instructions de montage sous-jacent effectuent l'obtention et la définition séparément.
en fait seulement maintenant je comprends ce que vous dites;) +1
Tout d'abord, il s'agit d'un comportement indéfini car les deux threads se lit et écrit de la même variable non atomique Avec cela étant dit, si vous voulez comprendre ce que votre ordinateur effectue réellement lorsque ce programme est exécuté, cela peut aider à supposer que VA code> sont potentiellement simultanés et ne se produisent pas non plus avant l'autre. P>
++ VA code> est identique au
VA = VA + 1 code>. En fait, la norme dit qu'ils sont identiques et le compilateur les compilera probablement de manière identique. Étant donné que votre programme contient UB, le compilateur n'est pas tenu de faire quelque chose de sensible comme à l'aide d'une instruction d'incrément atomique. Si vous vouliez une instruction d'incrémentation atomique, vous auriez dû faire
VA code> atomique. De même,
- VA code> est identique à celui
VA = VA - 1 code>. Donc, dans la pratique, divers résultats sont possibles. P>
Je ne sais pas ce qui a besoin de clarifier. Vous vous dites que c'est une course. Les courses introduisent un comportement non défini. Tout peut arriver.
Les deux threads tentent de modifier
VA code> en même temps b>, même à l'intérieur de la boucle. Qui gagne? Unkknow, la valeur finale de
VA code> peut être n'importe quoi.
Je soupçonne que vous croyez que les opérations d'incrément et de décrémentation sont atomiques. Ils ne sont pas.
Même si l'opération est atomique, sans synchronisation, il n'y a aucune garantie si ou lorsque des modifications apportées à
VA code> sont réellement poussées à un endroit où d'autres threads peuvent les voir. Par exemple,
VA code> peut très bien stocké dans un registre.
BTW, optimiseur est autorisé à modifier cette méthode comme
VA + = 10000; code>,
VA + = 10000; code>, réduisant encore plus de "chances" pour voir l'effet de course.
Notez qu'une course de données est une propriété statique d'un programme. Ce n'est pas un comportement.
Notez que dans l'ordre général compte également si vous synchronisez correctement l'accès. Supposer qu'un thread ferait
va = 0; code> et l'autre
VA = 1; code> puis dans un code correct (pas de course de données), vous ne savez pas nécessairement quel thre thread