0
votes

Condition de course lors de l'incrémentation et de la décrémentation de la variable globale en C ++

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;
}


7 commentaires

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 en même temps , même à l'intérieur de la boucle. Qui gagne? Unkknow, la valeur finale de VA 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 sont réellement poussées à un endroit où d'autres threads peuvent les voir. Par exemple, VA peut très bien stocké dans un registre.


BTW, optimiseur est autorisé à modifier cette méthode comme VA + = 10000; , VA + = 10000; , 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; et l'autre VA = 1; puis dans un code correct (pas de course de données), vous ne savez pas nécessairement quel thre thread


3 Réponses :


4
votes

La norme dit (citant le dernier brouillon):

[intro.Races]

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.

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 .

Votre exemple de programme a une course de données et le comportement du programme est indéfini.

Ce que je ne comprends pas, c'est comment l'ordre des opérations comporte dans cet exemple.

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.

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 attribuée

La même chose s'applique à l'opérateur d'incrément. La machine abstraite:

  • Lire une valeur de la mémoire
  • incrément la valeur
  • Écrivez une valeur à la mémoire

    Il y a plusieurs étapes là-bas qui peuvent interlaisser avec des opérations dans l'autre thread.

    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.

    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 ++.


0 commentaires

4
votes

L'idée importante ici est que lorsque C ++ est compilé, il est "traduit" en langage de montage. La traduction de ++ va ou - VA entraînera un code d'assemblage qui déplace la valeur de VA à un registre, puis stocke le résultat d'ajouter 1 à ce registre Retour à VA dans une instruction distincte. De cette manière, c'est exactement la même chose que VA = VA + 1; . Cela signifie également que l'opération VA ++ n'est pas nécessairement atomique .

voir ici pour une explication de ce que le code de montage pour ces instructions ressemblera.

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): xxx

référence: https://fr.cppreference.com/w/cpp/atomic/atomic


3 commentaires

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



3
votes

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 VA sont potentiellement simultanés et ne se produisent pas non plus avant l'autre.

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 est identique au VA = VA + 1 . 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 atomique. De même, - VA est identique à celui VA = VA - 1 . Donc, dans la pratique, divers résultats sont possibles.


0 commentaires