Même si votre compilateur a toujours émis cette opération comme une opération atomique, accéder à num
de tout autre thread simultanément constituerait une course aux données selon les normes C++11 et C++14 et le programme aurait un comportement indéfini.
Mais c'est pire que ça. Premièrement, comme cela a été mentionné, l'instruction générée par le compilateur lors de l'incrémentation d'une variable peut dépendre du niveau d'optimisation. Deuxièmement, le compilateur peut réordonner autre les accès à la mémoire autour ++num
si num
n'est pas atomique, par exemple
int main()
{
std::unique_ptr<std::vector<int>> vec;
int ready = 0;
std::thread t{[&]
{
while (!ready);
// use "vec" here
});
vec.reset(new std::vector<int>());
++ready;
t.join();
}
Même si nous supposons de manière optimiste que ++ready
est "atomique", et que le compilateur génère la boucle de vérification selon les besoins (comme je l'ai dit, c'est UB et donc le compilateur est libre de la supprimer, de la remplacer par une boucle infinie, etc.), le compilateur pourrait encore déplacer l'affectation du pointeur, ou pire encore l'initialisation de l'objet vector
à un point situé après l'opération d'incrémentation, provoquant le chaos dans le nouveau thread. Dans la pratique, je ne serais pas du tout surpris qu'un compilateur optimisateur supprime l'option ready
et la boucle de vérification complètement, car cela n'affecte pas le comportement observable selon les règles du langage (par opposition à vos espoirs privés).
En fait, lors de la conférence Meeting C++ de l'année dernière, j'ai entendu parler de dos les développeurs de compilateurs qu'ils mettent très volontiers en œuvre des optimisations qui font que des programmes multithreads écrits naïvement se comportent mal, tant que les règles du langage le permettent, si une amélioration même mineure des performances est constatée dans des programmes correctement écrits.
Enfin, même si si vous ne vous souciiez pas de la portabilité et que votre compilateur était magiquement gentil, le processeur que vous utilisez est très probablement de type CISC superscalaire et décomposera les instructions en micro-opérations, les réordonnera et/ou les exécutera de manière spéculative, dans une mesure uniquement limitée par des primitives de synchronisation telles que (sur Intel) la fonction LOCK
préfixe ou clôtures de mémoire, afin de maximiser les opérations par seconde.
Pour faire court, les responsabilités naturelles de la programmation thread-safe sont les suivantes :
- Votre devoir est d'écrire du code qui a un comportement bien défini selon les règles du langage (et en particulier le modèle de mémoire standard du langage).
- Le devoir de votre compilateur est de générer du code machine qui a le même comportement bien défini (observable) sous le modèle de mémoire de l'architecture cible.
- Le devoir de votre CPU est d'exécuter ce code de sorte que le comportement observé soit compatible avec le modèle de mémoire de sa propre architecture.
Si vous voulez le faire à votre façon, cela peut fonctionner dans certains cas, mais comprenez que la garantie est annulée et que vous serez le seul responsable de tout dommage causé par l'utilisation de ce produit. indésirable résultats. :-)
PS : Exemple correctement rédigé :
int main()
{
std::unique_ptr<std::vector<int>> vec;
std::atomic<int> ready{0}; // NOTE the use of the std::atomic template
std::thread t{[&]
{
while (!ready);
// use "vec" here
});
vec.reset(new std::vector<int>());
++ready;
t.join();
}
C'est sûr parce que :
- Les contrôles de
ready
ne peuvent pas être éliminés par optimisation selon les règles de la langue.
- El
++ready
se passe avant le chèque qui voit ready
comme non nulle, et les autres opérations ne peuvent pas être réorganisées autour de ces opérations. Ceci est dû au fait que ++ready
et le contrôle sont séquentiellement cohérent qui est un autre terme décrit dans le modèle de mémoire C++ et qui interdit ce réordonnancement spécifique. Par conséquent, le compilateur ne doit pas réordonner les instructions et doit également indiquer au CPU qu'il ne doit pas, par exemple, reporter l'écriture à vec
après l'incrément de ready
. Cohérence séquentielle est la garantie la plus forte concernant les atomiques dans la norme de langage. Des garanties moindres (et théoriquement moins chères) sont disponibles, par exemple, via d'autres méthodes de l'option std::atomic<T>
mais elles sont réservées aux experts et ne sont pas optimisées par les développeurs de compilateurs, car elles sont rarement utilisées.
68 votes
Qui vous a dit que
add
est atomique ?6 votes
Étant donné que l'une des caractéristiques de l'atomique est la prévention de types spécifiques de réordonnancement pendant l'optimisation, non, indépendamment de l'atomicité de l'opération réelle
1 votes
Il se peut que votre compilateur voit que num++ peut être optimisé à cela parce que vous n'utilisez pas la valeur de retour de num++.
2 votes
Il doit encore charger num, l'augmenter puis le réécrire en mémoire
20 votes
Je tiens également à souligner que si ce qui est atomique sur votre plateforme, il n'y a aucune garantie qu'il le sera sur une autre pltaforme. Soyez indépendant de la plateforme et exprimez votre intention en utilisant un fichier de type
std::atomic<int>
.9 votes
Pendant l'exécution de cette
add
un autre noyau pourrait voler cette adresse mémoire dans le cache de ce noyau et la modifier. Sur un processeur x86, leadd
l'enseignement a besoin d'unelock
préfixe si l'adresse doit être verrouillée dans le cache pendant la durée de l'opération.21 votes
Il est possible pour cualquier pour que l'opération soit "atomique". Il suffit d'avoir de la chance et de ne jamais exécuter quoi que ce soit qui puisse révéler qu'elle n'est pas atomique. Atomique n'a de valeur que comme garantie . Étant donné que vous regardez le code d'assemblage, la question est de savoir si cette architecture particulière vous fournit la garantie y si le compilateur fournit une garantie que c'est l'implémentation au niveau de l'assemblage qu'ils choisissent.
2 votes
Dans ce cas, votre fonction telle qu'elle est est parfaitement sûre, car les autres threads n'ont aucun moyen d'accéder à la pile de données
num
variable. Mais ce n'est probablement pas ce que vous demandez. (Il serait peut-être préférable que votrenum
étaient une variable globale).0 votes
Donner l'impression de fonctionner est une réponse valable à l'invocation d'un comportement indéfini.
0 votes
@Lmn : Les questions de SO devraient utiliser des blocs de code pour le code, pas des images de texte. La compacité de l'image était agréable, mais il y a suffisamment de raisons de ne pas utiliser d'images pour que vous laissiez la modification de Joseph s'appliquer. (Les utilisateurs aveugles avec des lecteurs d'écran, les pare-feu d'entreprise qui bloquent les images, etc.) Dans d'autres cas, la possibilité pour les moteurs de recherche de rechercher le code est pertinente ; ce n'est pas le cas ici mais les autres raisons sont suffisantes.