J'utilise Cygwin GCC et j'exécute ce code:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
unsigned u = 0;
void foo()
{
u++;
}
int main()
{
vector<thread> threads;
for(int i = 0; i < 1000; i++) {
threads.push_back (thread (foo));
}
for (auto& t : threads) t.join();
cout << u << endl;
return 0;
}
Compilé avec la ligne: g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o
.
Il imprime 1000, ce qui est correct. Cependant, je m'attendais à un nombre inférieur en raison de l'écrasement des threads sur une valeur précédemment incrémentée. Pourquoi ce code ne souffre-t-il pas d'un accès mutuel?
Ma machine de test a 4 cœurs, et je n'ai mis aucune restriction sur le programme que je connais.
Le problème persiste lors du remplacement du contenu du partagé foo
par quelque chose de plus complexe, par exemple
if (u % 3 == 0) {
u += 4;
} else {
u -= 1;
}
c++
race-condition
mafu
la source
la source
u
en mémoire. Le CPU fera en fait des choses étonnantes comme remarquer que la ligne de mémoire pouru
n'est pas dans le cache du CPU et il redémarrera l'opération d'incrémentation. C'est pourquoi passer de x86 à d'autres architectures peut être une expérience révélatrice!while true; do res=$(./a.out); if [[ $res != 1000 ]]; then echo $res; break; fi; done;
imprime 999 ou 998 sur mon système.Réponses:
foo()
est si court que chaque thread se termine probablement avant même que le suivant ne soit généré. Si vous ajoutez un sommeil pendant un temps aléatoirefoo()
avant leu++
, vous pouvez commencer à voir ce que vous attendez.la source
Il est important de comprendre qu'une condition de concurrence critique ne garantit pas que le code fonctionnera de manière incorrecte, mais simplement qu'il pourrait faire n'importe quoi, car il s'agit d'un comportement non défini. Y compris courir comme prévu.
En particulier sur les machines X86 et AMD64, les conditions de course causent rarement des problèmes dans certains cas, car la plupart des instructions sont atomiques et les garanties de cohérence sont très élevées. Ces garanties sont quelque peu réduites sur les systèmes multiprocesseurs où le préfixe de verrouillage est nécessaire pour que de nombreuses instructions soient atomiques.
Si l'incrémentation de votre machine est une opération atomique, cela fonctionnera probablement correctement même si selon la norme du langage, il s'agit d'un comportement non défini.
Plus précisément, je pense que dans ce cas, le code peut être compilé en une instruction atomique Fetch and Add (ADD ou XADD dans l'assemblage X86) qui est en effet atomique dans les systèmes à processeur unique, mais sur les systèmes multiprocesseurs, cela n'est pas garanti comme atomique et verrouillé serait nécessaire pour le faire. Si vous utilisez un système multiprocesseur, il y aura une fenêtre où les threads pourraient interférer et produire des résultats incorrects.
Plus précisément, j'ai compilé votre code en assemblage en utilisant https://godbolt.org/ et
foo()
compile en:Cela signifie qu'il exécute uniquement une instruction d'ajout qui pour un seul processeur sera atomique (bien que comme mentionné ci-dessus, ce n'est pas le cas pour un système multiprocesseur).
la source
inc [u]
n'est pas atomique. LeLOCK
préfixe est nécessaire pour rendre une instruction vraiment atomique. L'OP a simplement de la chance. Rappelez-vous que même si vous dites au CPU "ajouter 1 au mot à cette adresse", le CPU doit toujours récupérer, incrémenter, stocker cette valeur et un autre CPU peut faire la même chose simultanément, ce qui rend le résultat incorrect.Je pense que ce n'est pas tellement le problème si vous vous endormez avant ou après le
u++
. C'est plutôt que l'opération seu++
traduit par du code qui est - comparé à la surcharge des threads générateurs qui appellentfoo
- très rapidement exécuté de sorte qu'il est peu probable qu'il soit intercepté. Cependant, si vous "prolongez" l'opérationu++
, la condition de concurrence deviendra beaucoup plus probable:résultat:
694
BTW: j'ai aussi essayé
et cela m'a donné la plupart du temps
1997
, mais parfois1995
.la source
else u -= 1
jamais être exécuté? Même dans un environnement parallèle, la valeur ne devrait jamais ne pas correspondre%2
, n'est-ce pas?else u -= 1
exécuté une fois, la première fois que foo () est appelée, lorsque u == 0. Les 999 fois restantes u sont impaires etu += 2
sont exécutées résultant en u = -1 + 999 * 2 = 1997; c'est-à-dire la sortie correcte. Une condition de concurrence entraîne parfois l'écrasement de l'un des + = 2 par un thread parallèle et vous obtenez 1995.Il souffre d'une condition de race. Mettre
usleep(1000);
avantu++;
dansfoo
et je vois une sortie différente (<1000) à chaque fois.la source
La réponse probablement pourquoi la condition de la course n'a pas manifesté pour vous, mais il ne exist, est que
foo()
est si rapide, par rapport au temps qu'il faut pour démarrer un fil, que chaque fil se termine avant le prochain pouvez même commencer. Mais...Même avec votre version originale, le résultat varie selon le système: je l'ai essayé à votre guise sur un Macbook (quad-core), et en dix courses, j'ai obtenu 1000 trois fois, 999 six fois et 998 une fois. La course est donc assez rare, mais clairement présente.
Vous avez compilé avec
'-g'
, ce qui permet de faire disparaître les bugs. J'ai recompilé votre code, toujours inchangé mais sans le'-g'
, et la course est devenue beaucoup plus prononcée: j'ai obtenu 1000 une fois, 999 trois fois, 998 deux fois, 997 deux fois, 996 une fois et 992 une fois.Ré. la suggestion d'ajouter un sommeil - cela aide, mais (a) un temps de sommeil fixe laisse les threads toujours biaisés par l'heure de début (sous réserve de la résolution de la minuterie), et (b) un sommeil aléatoire les étale lorsque ce que nous voulons est de rapprochez-les. Au lieu de cela, je les code pour attendre un signal de départ, afin que je puisse les créer tous avant de les laisser travailler. Avec cette version (avec ou sans
'-g'
), j'obtiens des résultats partout, aussi bas que 974 et pas plus élevés que 998:la source
-g
drapeau ne "fait en aucun cas disparaître les bogues". L'-g
indicateur sur les compilateurs GNU et Clang ajoute simplement des symboles de débogage au binaire compilé. Cela vous permet d'exécuter des outils de diagnostic comme GDB et Memcheck sur vos programmes avec une sortie lisible par l'homme. Par exemple, lorsque Memcheck est exécuté sur un programme avec une fuite de mémoire, il ne vous indiquera pas le numéro de ligne à moins que le programme n'ait été construit en utilisant l'-g
indicateur.-O2
au lieu de-g
». Mais cela dit, si vous n'avez jamais eu la joie de chasser un bogue qui ne se manifesterait qu'une fois compilé sans-g
, considérez-vous chanceux. Cela peut arriver, avec certains des bogues d'aliasing subtils les plus méchants. Je l' ai vu, mais pas récemment, et je pourrais croire que c'était peut-être une bizarrerie d'un vieux compilateur propriétaire, donc je vais vous croire, provisoirement, à propos des versions modernes de GNU et Clang.-g
ne vous empêche pas d'utiliser les optimisations. par exemple,gcc -O3 -g
fait le même asm quegcc -O3
, mais avec des métadonnées de débogage. gdb dira cependant "optimisé" si vous essayez d'imprimer certaines variables.-g
pourrait peut-être changer les emplacements relatifs de certaines choses en mémoire, si l'un des éléments qu'il ajoute fait partie de la.text
section. Cela prend certainement de la place dans le fichier objet, mais je pense qu'après la liaison, tout se termine à une extrémité du segment de texte (pas de section), ou ne fait pas du tout partie d'un segment. Peut-être pourrait affecter où les choses sont mappées pour les bibliothèques dynamiques.