3
votes

Est-il Thread Safe de lire à partir des éléments d'index inférieurs d'un tableau struct pendant qu'il est rempli de données dans le thread principal

Question d'origine:

J'ai obtenu un tableau de structures et je l'ai rempli dans un thread séparé en le lisant dans le thread principal:

std::atomic<int> FilledIndex;
    void FillingMyData(struct DataModel myData[])
    {
      for(size_t i = 0; i < 1024; i++)
      {
        myData[i].a = rand();
        myData[i].b = rand();
        myData[i].IsFilled = true;

    FilledIndex = i;
  }
}

int main()
{
     std::thread ReadThread(FillingMyData, MyData);
     while(FilledIndex < 1024)
     {
          std::cout << MyData[FilledIndex].a;
     }
     ReadThread.join();
     return 0;
}
  • J'ai un thread qui remplit le tableau Mydata de 0 index au dernier index (au-dessus, il est 1024).

  • Ensuite, j'obtiens le dernier index struct rempli du thread de remplissage.

  • Et puis j'essaye de lire les valeurs de l'élément avec un index inférieur à celui rempli.

  • Supposons que lorsque le 500e élément est rempli, je lis la valeur de l'élément 499 du tableau MyData , donc je m'assure que je ne lis pas l'élément du tableau qui est en cours d'écriture .

Q1: Ce fil est-il sûr?

Q2: y a-t-il un risque de comportement non défini ou de mauvaise lecture des valeurs?


Autres modifications: p>

La question n'a pas été correctement modifiée pour ajouter plus de détails et c'est pourquoi elle a introduit des incohérences dans les réponses, j'ai donc séparé les modifications précédentes pour améliorer la cohérence des réponses et des réponses acceptées.

Modifier 1 : ceci est une suggestion de mise en œuvre possible. bien que cela puisse montrer de faux résultats, mais je voulais juste poser des questions sur la sécurité des threads et un comportement indéfini, la solution suivante peut montrer différents résultats, mais j'essayais d'abord de poser des questions sur la sécurité des threads.

struct DataModel MyData[1024];

struct DataModel
{
    bool IsFilled;
    float a;
    float b;
}


14 commentaires

"Ensuite, je récupère le dernier index struct rempli du thread de remplissage." Qu'est-ce que cela signifie?


Recommandation: Apprenez à utiliser le nettoyant pour fils de clang. Cela vous donnera une réponse définitive en cas de doute.


mon environnement de développement est vscode comme IDE et nuwen MinGW64 build pour la compilation sous windows, est-il possible d'utiliser clang thread-sanitizer fonctionnant dans cet environnement?


@TheQuantumPhysicist Il ne vous donne pas de réponses définitives. Le simple fait qu'il ne trouve rien ne signifie pas qu'il n'y a pas de problème de filetage.


@yekanchi je ne sais pas. Désolé. Je ne code pas beaucoup sur Windows.


Quelle est la valeur initiale de FilledIndex ?


@MaxLanghof Veuillez préciser. Êtes-vous en train de dire qu'il peut y avoir des courses de données que thread-sanitizer peut ne pas détecter? Veuillez fournir des exemples.


@TheQuantumPhysicist En plus de cela mentionnant qu'il peut manquer des courses de données (et ce n'est que l'une des plusieurs façons), il existe également d'autres types de conditions de course qui ne sont pas des courses de données. Le simple fait que vos accès soient synchronisés ne signifie pas qu'ils se produisent dans l'ordre que vous souhaitez réellement.


@TheQuantumPhysicist Ici en est une autre. Le message de validation nous indique que ce test simple est floconneux lorsque vous ajoutez un sommeil au lieu de la barrière (sorte d'ironie). Aussi, pourquoi utiliser les paramètres " avec 3x (analyse moins précise) et 9x (plus analyse précise) overhead " s'il a déjà détecté toutes les courses de toute façon? Ne vous méprenez pas, il est payant de pouvoir utiliser un désinfectant pour fils, mais une course propre n'est pas une preuve de la sécurité des fils.


@TheQuantumPhysicist Il est essentiellement évident qu'aucun outil ne peut détecter toutes les courses de données.


Pouvez-vous expliquer la logique de votre code: qu'essayez-vous de faire et comment essayez-vous de le faire? Quel est le but de while (FilledIndex <1024) ?


Modifier de manière significative une question, avec un code source différent, après que les réponses aient été publiées, sans un très gros avertissement indiquant que les changements sont importants, est un grand non !


@yekanchi Veuillez poser une nouvelle question si vous souhaitez poser des questions sur un code différent.


bool IsFilled; est juste un gaspillage de 4 octets (y compris le remplissage pour l'alignement) parce que vous remplissez l'ordre. Sans être atomique<> , c'est totalement inutile car vous ne pouvez pas le lire à moins que vous ne sachiez déjà qu'aucun autre thread ne peut l'écrire. Mais vous ne voulez pas vraiment le rendre atomique, cela peut vaincre la vectorisation automatique une fois que tout le tableau est écrit. Le FilledIndex atomique n'est pas merveilleux, surtout avec un magasin seq_cst à l'intérieur de la boucle (beaucoup plus lent qu'un magasin memory_order_release ).


4 Réponses :


8
votes

Oui, vous pouvez travailler en toute sécurité sur des objets séparés dans le même tableau. Même si un tableau est un objet, ce sont les éléments du tableau sur lesquels nous travaillons et ce sont des objets séparés. Tant que vous ne lisez pas un élément sur lequel l'écrivain écrit, il n'y a pas de course aux données et le code a défini un comportement. Vous avez des problèmes de synchronisation avec votre code publié, mais les autres réponses ici couvrent ce sont celles-ci.

Ce qui peut arriver ici, c'est ce que l'on appelle faux partage . Ce qui se passe dans ces cas, c'est que les objets séparés se trouvent dans la même ligne de cache en mémoire. Lorsque le noyau / thread change un objet, cette ligne de cache est marquée comme modifiée. Cela signifie que l'autre noyau / thread doit resynchroniser cette ligne pour apporter des modifications, ce qui signifie que les deux cœurs / threads ne peuvent pas s'exécuter en même temps. Ceci est une pénalité de performance uniquement, et le programme donnera toujours les bons résultats, il sera juste plus lent.


5 commentaires

@ FrançoisAndrieux Par défaut, c'est le cas. Si vous l'utilisez avec un modèle de mémoire différent, cela dépend. Puisque l'utilisateur dit que le fil d'écriture donne l'index au fil de lecture, j'ai senti qu'il n'était pas pertinent pour la question / réponse.


Bon point, la partie std :: atomic du code pourrait être supprimée et le reste serait toujours autonome.


@ FrançoisAndrieux je pense que std :: Atomic est nécessaire pour FilledIndex mais je l'utilisais par erreur pour IsFilled


@yekanchi Eh bien, un std::atomic n'est pas nécessaire pour la solution décrite dans la question. Et pour communiquer les indices prêts à être lus, il existe de nombreuses solutions possibles, std::atomic étant l'une d'entre elles mais pas la seule.


@ FrançoisAndrieux Merci pour la mise à jour. J'ai mis à jour la réponse pour parler du cas général et dire qu'ils devraient voir les autres réponses sur leur code spécifique.



3
votes

Le code tel qu'il est écrit n'est pas nécessairement sûr et peut ne rien faire d'utile.

  • la valeur initiale de FilledIndex est zéro, donc il peut lire les données à partir de l'index zéro avant l'écriture, y compris éventuellement les valeurs partiellement écrites. Vous voudrez probablement le définir sur -1 et attendre qu'il soit> = 0 avant la sortie.
  • il n'y a rien pour empêcher le thread principal de s'exécuter indéfiniment, affichant la même valeur à zéro pour toujours - c'est au planificateur; rien n'empêche le thread principal de sortir la valeur à un index donné plus d'une fois. Vous voudrez probablement compter de zéro à l'index rempli plutôt que d'afficher la valeur à l'index rempli.

Les préoccupations ci-dessus peuvent signifier que vous choisissez un moyen différent pour communiquer la valeur de l'index rempli au fil de discussion principal.


1 commentaires

Ce sont des problèmes avec le code, mais il y a encore d'autres problèmes. " il n'y a rien non plus pour empêcher le thread principal de sortir la valeur à un index donné plus d'une fois " OTOH rien ne garantit qu'un élément en particulier est imprimé du tout, car le compteur peut avoir avancé à côté de lui!



4
votes

Le code a une course aux données et continuera à tourner indéfiniment.

La course aux données se produit parce que la valeur initiale de FilledIndex est 0 donc, la première fois dans la boucle, vous lisez à partir du même index dans lequel vous écrivez (car i == 0 ).

La boucle ne se terminera jamais car i n'atteindra jamais la valeur de fin - la boucle se terminera avant de définir FilledIndex sur 1024 .


3 commentaires

Vous avez raison de dire que cela fonctionne pour toujours, mais il n'y a pas de course aux données sur la première itération car FilledIndex est atomique. Oui, c'est une condition de concurrence à chaque itération (la sortie du programme dépend fortement de la planification), mais pas une course aux données.


@MaxLanghof Lors de la première itération pour les deux threads FilledIndex == 0 et i == 0 . Ce qui signifie que les deux boucles s’exécuteront en même temps sur le même index du tableau. Ensuite, le thread de travail définira explicitement FilledIndex = 0 et commencera à travailler sur l'itération i == 1 de sa boucle pendant que le thread de sortie continue FilledIndex == 0 données. Donc, jusqu'à ce que le thread de travail commence sa deuxième itération , les deux threads accèdent aux mêmes données dans une condition de concurrence.


Une seule itération avec une race data et aussi une condition race à chaque itération! " la boucle se terminera avant de définir FilledIndex sur 1024. " avec un peu de chance, ce serait encore un autre bogue



1
votes

Je pense que ce code n'est certainement pas thread-safe.

Tout d'abord, la variable FilledIndex n'est pas initialisée: comme indiqué par cplusplus.com si vous ne soumettez aucune valeur au constructeur, la variable atomique est laissée dans un état non initialisé. Cela peut entraîner des comportements inattendus.

Un autre problème concerne la condition de sortie dans le thread principal, car l'instruction for dans ReadThread boucle jusqu'en 1023, donc FilledIndex n'assumera jamais la valeur 1024 et la valeur main le fil ne se termine jamais.

Mais le principal problème est l'imprévisibilité de la planification de vos threads: qu'est-ce qui garantit que le ReadThread est exécuté après le principal? Rien!

Vous ne pouvez donc pas être sûr que vous bouclez sur toutes les valeurs du tableau. En fait, si vous essayez d'exécuter plusieurs fois votre programme, vous verrez que la sortie à chaque fois est différente et que différentes valeurs du tableau sont affichées.

Par exemple, si nous nommons ReadThread comme T , le thread principal comme M et le tableau comme A , voici les horaires possibles (en supposant que A de taille 5 pour semplicity):

  • T T T M T la sortie sera A [2]
  • M M T M T la sortie sera A [0] A [0] A [1]

En fait, vous imprimez un [ FilledIndex ] et vous ne pouvez pas prédire comment FilledIndex sera mis à jour, car cela dépend de la planification des threads.

J'espère que vous comprendrez ce que j'essaie de dire. Pour toute question ou clarification, évidemment je suis là! Je vous répondrai dès que possible!


5 commentaires

" donc FilledIndex n'assumera jamais la valeur 1024 " OTOH est une exigence du thread consommateur qu'il ne devrait jamais atteindre 1024, donc corriger ce bogue dans le producteur en introduirait un autre dans le consommateur. Aie!


Si le fil de lecture du lecteur est une "barre de progression", il n'y a rien de mal à imprimer la chose la plus récente. Il existe des cas d'utilisation possibles pour cela. Sinon, vous voudrez probablement lire le tableau un à la fois et vérifier uniquement que vous n'avez pas encore atteint FilledIndex. (Et si c'est le cas, faites tourner, attendez, cédez ou dormez, jusqu'à ce qu'il augmente.)


@PeterCordes c'est vrai, mais d'après la question, je n'ai pas compris que c'était le cas. Ce que je comprends, c'est: j'ai un tableau, un thread producteur qui le remplit et un thread consommateur qui le lit. Le code rapporté lit différentes valeurs du tableau dans des ordres différents chaque fois que vous l'exécutez, donc avec votre hypothèse, cela peut (ou non - je n'ai pas le temps de le tester maintenant) être correct. Je pense qu'il veut lire tout le tableau ...


Je pense (j'espère) que le lecteur n'est qu'un exemple trivial dans le cadre d'un MCVE. Des ordres différents, cependant? L'ordre est toujours monotone, c'est juste quels éléments sont sautés / non sautés qui ne sont pas déterministes. Vous ne pouvez jamais imprimer un élément haut avant un élément bas.


@PeterCordes vous avez raison! C'est sûrement monotone, je me suis juste mal exprimé! J'y pensais mais j'ai écrit autre chose! Merci pour la clarification.