Un entier peut-il être partagé entre les threads en toute sécurité?

Y at-il un problème avec plusieurs threads utilisant le même emplacement de mémoire entier entre les pthreads dans un programme C sans utilitaire de synchronisation?

Pour simplifier le problème,

  • Un seul thread va écrire dans l’entier
  • Plusieurs threads vont lire l’entier

Ce pseudo-C illustre ce que je pense

void thread_main(int *a) { //wait for something to finish //dereference 'a', make decision based on its value } int value = 0; for (int i=0; i<10; i++) pthread_create(NULL,NULL,thread_main,&value); } // do something value = 1; 

Je suppose que c’est sûr, car un entier occupe un mot du processeur, et lire / écrire dans un mot devrait être l’opération la plus atomique, non?

    Votre pseudo-code n’est PAS sécurisé.

    Bien que l’access à un entier de la taille d’un mot soit un atome, ce qui signifie que vous ne verrez jamais de valeur intermédiaire, mais que ce soit “avant écriture” ou “après écriture”, cela ne suffit pas pour votre algorithme décrit.

    Vous vous fiez à l’ordre relatif de l’écriture pour a et effectuez un autre changement qui réveille le fil. Ce n’est pas une opération atomique et n’est pas garanti sur les processeurs modernes.

    Vous avez besoin d’une sorte de barrière de mémoire pour empêcher la réorganisation d’écriture. Sinon, il n’est pas garanti que les autres threads verront JAMAIS la nouvelle valeur.

    Contrairement à Java où vous démarrez explicitement un thread, les threads posix commencent à s’exécuter immédiatement.
    Il n’y a donc aucune garantie que la valeur que vous définissez sur 1 dans la fonction principale (en supposant que ce soit ce que vous référez dans votre pseudocode) soit exécutée avant ou après que les threads tentent d’y accéder.
    Ainsi, bien qu’il soit sûr de lire l’entier simultanément, vous devez effectuer une synchronisation si vous devez écrire dans la valeur pour pouvoir être utilisé par les threads.
    Sinon, rien ne garantit quelle sera la valeur qu’ils liront (pour pouvoir agir en fonction de la valeur indiquée).
    Vous ne devriez pas émettre d’hypothèses sur le multithreading, par exemple qu’il existe un traitement dans chaque thread avant d’accéder à la valeur, etc.
    Il n’y a aucune garantie

    Je ne compterais pas dessus. Le compilateur peut émettre un code qui suppose qu’il sait quelle est la valeur de ‘valeur’ ​​à un moment donné dans un registre de la CPU sans le recharger de la mémoire.

    EDIT: Ben a raison (et je suis idiot de dire que ce n’était pas le cas), il est possible que le cpu réorganise les instructions et les exécute simultanément dans plusieurs pipelines. Cela signifie que la valeur = 1 pourrait éventuellement être définie avant que le pipeline effectuant le “travail” ne soit terminé. Pour ma défense (ce n’est pas un idiot?), Je n’ai jamais vu cela se produire dans la vraie vie. Nous avons une bibliothèque de threads étendue et nous effectuons des tests exhaustifs à long terme et ce modèle est utilisé partout. Je l’aurais vu si cela se produisait, mais aucun de nos tests ne s’est écrasé ni ne donne une mauvaise réponse. Mais … Ben a raison, la possibilité existe. Cela se produit probablement tout le temps dans notre code, mais la réorganisation ne définit pas les indicateurs suffisamment tôt pour que les utilisateurs des données protégées par ces indicateurs puissent utiliser les données avant qu’elles ne soient terminées. Je modifierai notre code pour y inclure les obstacles, car rien ne garantit que cela continuera de fonctionner dans la nature. Je crois que la solution correcte est semblable à ceci:

    Discussions qui lisent la valeur:

     ... if (value) { __sync_synchronize(); // don't pipeline any of the work until after checking value DoSomething(); } ... 

    Le fil qui définit la valeur:

     ... DoStuff() __sync_synchronize(); // Don't pipeline "setting value" until after finishing stuff value = 1; // Stuff Done ... 

    Cela étant dit, j’ai trouvé que c’était une simple explication des obstacles.

    BARRIÈRE DU COMPILATEUR Les barrières de mémoire affectent le processeur. Les obstacles du compilateur affectent le compilateur. Volatile ne gardera pas le compilateur de réorganiser le code. Ici pour plus d’informations.

    Je pense que vous pouvez utiliser ce code pour empêcher gcc de le réorganiser pendant la compilation:

     #define COMPILER_BARRIER() __asm__ __volatile__ ("" ::: "memory") 

    Alors peut-être que c’est ce qui devrait vraiment être fait?

     #define GENERAL_BARRIER() do { COMPILER_BARRIER(); __sync_synchronize(); } while(0) 

    Discussions qui lisent la valeur:

     ... if (value) { GENERAL_BARRIER(); // don't pipeline any of the work until after checking value DoSomething(); } ... 

    Le fil qui définit la valeur:

     ... DoStuff() GENERAL_BARRIER(); // Don't pipeline "setting value" until after finishing stuff value = 1; // Stuff Done ... 

    Utiliser GENERAL_BARRIER () empêche gcc de réorganiser le code et empêche le cpu de réorganiser le code. Maintenant, je me demande si gcc ne réordonnera pas le code sur sa barrière de mémoire intégrée, __sync_synchronize (), ce qui rendrait l’utilisation de COMPILER_BARRIER redondante.

    X86 Comme Ben le fait remarquer, différentes architectures ont des règles différentes concernant la manière dont elles réarrangent le code dans les pipelines d’exécution. Intel semble être assez conservateur. Donc, les obstacles ne sont peut-être pas autant nécessaires sur Intel. Ce n’est pas une bonne raison pour éviter les obstacles, car cela pourrait changer.

    POSTE ORIGINAL: Nous faisons cela tout le temps. c’est parfaitement sûr (pas pour toutes les situations, mais beaucoup). Notre application fonctionne sur des milliers de serveurs dans une immense batterie de serveurs avec 16 instances par serveur et nous n’avons pas de conditions de concurrence. Vous avez raison de vous demander pourquoi les gens utilisent des mutex pour protéger des opérations déjà atomiques. Dans de nombreuses situations, le locking est une perte de temps. La lecture et l’écriture sur des entiers 32 bits sur la plupart des architectures est atomique. N’essayez pas cela avec des champs de bits 32 bits!

    La réorganisation du processeur en écriture n’affectera pas un thread qui lit une valeur globale définie par un autre thread. En fait, le résultat en utilisant des verrous est le même que le résultat non sans verrous. Si vous gagnez la course et vérifiez la valeur avant qu’elle ne soit modifiée … eh bien, c’est la même chose que de gagner la course pour la verrouiller afin que personne ne puisse la modifier pendant que vous la lisez. Fonctionnellement le même.

    Le mot-clé volatile indique au compilateur de ne pas stocker de valeur dans un registre, mais de continuer à se référer à l’emplacement de mémoire d’origine. cela ne devrait avoir d’effet que si vous optimisez le code. Nous avons constaté que le compilateur est assez intelligent à ce sujet et ne nous sums pas encore retrouvés dans une situation où volatile changeait rien. Le compilateur semble assez doué pour proposer des candidats à l’optimisation des registres. Je soupçonne que le mot clé const pourrait encourager l’optimisation du registre sur une variable.

    Le compilateur peut réorganiser le code dans une fonction s’il sait que le résultat final ne sera pas différent. Je n’ai pas vu le compilateur faire cela avec des variables globales, car le compilateur n’a aucune idée de la manière dont le fait de modifier l’ordre d’une variable globale affectera le code en dehors de la fonction immédiate.

    Si une fonction est active, vous pouvez contrôler le niveau d’optimisation au niveau de la fonction à l’aide de __attrute_.

    Cela dit, si vous utilisez cet indicateur en tant que passerelle pour autoriser un seul thread du groupe à effectuer certains travaux, cela ne fonctionnera pas . Exemple: les fils A et B pourraient lire l’indicateur. Le fil A est programmé. Le fil B met l’indicateur à 1 et commence à fonctionner. Le fil A se réveille et met l’indicateur à 1 et commence à fonctionner. Ooops! Pour éviter les lockings et continuer à faire quelque chose comme ça, vous devez vous pencher sur les opérations atomiques, en particulier les commandes génériques atomiques de type gcc telles que __sync_bool_compare_and_swap (valeur, ancien, nouveau). Cela vous permet de définir value = new si la valeur est actuellement ancienne. Dans l’exemple précédent, si valeur = 1, un seul thread (A ou B) pouvait exécuter __sync_bool_compare_and_swap (& value, 1, 2) et modifier la valeur de 1 à 2. Le thread qui perdait échouait. __sync_bool_compare_and_swap renvoie le succès de l’opération.

    Au fond, il y a un “verrou” lorsque vous utilisez les commandes internes atomiques, mais il s’agit d’une instruction matérielle et très rapide par rapport à l’utilisation de mutex.

    Cela dit, utilisez des mutex quand vous devez changer beaucoup de valeurs en même temps. Les opérations atomiques (à ce jour) ne fonctionnent que lorsque toutes les données devant être modifiées de manière atomique peuvent être contenues dans un format 8,16,32,64 ou 128 bits contigu.

    Supposons que la première chose que vous fassiez dans thread func soit en sumil une seconde. Donc, la valeur après sera définitivement 1.

    À tout moment, vous devriez au moins déclarer la variable partagée volatile . Cependant, dans tous les cas, vous devriez préférer une autre forme de thread IPC ou de synchronisation; dans ce cas, il semble qu’une variable de condition correspond à ce dont vous avez réellement besoin.

    Hm, je suppose que c’est sécurisé, mais pourquoi ne déclarez-vous pas simplement une fonction qui renvoie la valeur aux autres threads, car ils ne la liront que?

    Parce que la simple idée de passer des pointeurs pour séparer les threads est déjà un échec de sécurité, à mon humble avis. Ce que je vous dis, c’est: pourquoi donner une adresse entière (modifiable, accessible au public) alors que vous n’avez besoin que de la valeur?