J'ai entendu dire que i ++ n'est pas une instruction thread-safe car dans l'assemblage, il se réduit à stocker la valeur d'origine en tant que température quelque part, à l'incrémenter, puis à la remplacer, ce qui pourrait être interrompu par un changement de contexte.
Cependant, je m'interroge sur ++ i. Pour autant que je sache, cela se réduirait à une seule instruction d'assemblage, telle que «ajouter r1, r1, 1» et comme il ne s'agit que d'une seule instruction, il serait ininterrompu par un changement de contexte.
Quelqu'un peut-il clarifier? Je suppose qu'une plate-forme x86 est utilisée.
c++
c
multithreading
Samoz
la source
la source
Réponses:
Vous avez mal entendu. Il se peut que ce
"i++"
soit thread-safe pour un compilateur et une architecture de processeur spécifiques, mais ce n'est pas du tout obligatoire dans les normes. En fait, comme le multi-threading ne fait pas partie des normes ISO C ou C ++ (a) , vous ne pouvez pas considérer que quoi que ce soit soit thread-safe en fonction de ce à quoi vous pensez qu'il sera compilé.Il est tout à fait possible de
++i
compiler en une séquence arbitraire telle que:qui ne serait pas thread-safe sur mon processeur (imaginaire) qui n'a pas d'instructions d'incrémentation de mémoire. Ou cela peut être intelligent et le compiler en:
où
lock
désactive etunlock
active les interruptions. Mais, même dans ce cas, cela peut ne pas être thread-safe dans une architecture qui a plus d'un de ces processeurs partageant la mémoire (lelock
peut désactiver uniquement les interruptions pour un processeur).Le langage lui-même (ou les bibliothèques pour lui, s'il n'est pas intégré au langage) fournira des constructions thread-safe et vous devriez les utiliser plutôt que de dépendre de votre compréhension (ou peut-être d'un malentendu) du code machine qui sera généré.
Des choses comme Java
synchronized
etpthread_mutex_lock()
(disponibles pour C / C ++ sous certains systèmes d'exploitation) sont ce que vous devez examiner (a) .(a) Cette question a été posée avant l'achèvement des normes C11 et C ++ 11. Ces itérations ont maintenant introduit la prise en charge des threads dans les spécifications du langage, y compris les types de données atomiques (bien qu'ils, et les threads en général, soient facultatifs, au moins en C).
la source
Vous ne pouvez pas faire une déclaration générale sur ++ i ou i ++. Pourquoi? Pensez à incrémenter un entier 64 bits sur un système 32 bits. À moins que la machine sous-jacente n'ait une instruction à quatre mots «charger, incrémenter, stocker», l'incrémentation de cette valeur nécessitera plusieurs instructions, dont chacune peut être interrompue par un changement de contexte de thread.
De plus, il
++i
n'est pas toujours «d'en ajouter un à la valeur». Dans un langage comme C, l'incrémentation d'un pointeur ajoute en fait la taille de l'objet pointé. Autrement dit, sii
est un pointeur vers une structure de++i
32 octets , ajoute 32 octets. Alors que presque toutes les plates-formes ont une instruction "incrémenter la valeur à l'adresse mémoire" qui est atomique, toutes n'ont pas une instruction atomique "ajouter une valeur arbitraire à la valeur à l'adresse mémoire".la source
Ils sont tous deux non sécurisés pour les threads.
Un processeur ne peut pas faire de calcul directement avec la mémoire. Il le fait indirectement en chargeant la valeur de la mémoire et en faisant le calcul avec les registres du processeur.
i ++
++ i
Dans les deux cas, il existe une condition de concurrence qui aboutit à la valeur i imprévisible.
Par exemple, supposons qu'il existe deux threads ++ i simultanés, chacun utilisant respectivement les registres a1, b1. Et, avec le changement de contexte exécuté comme suit:
En conséquence, i ne devient pas i + 2, il devient i + 1, ce qui est incorrect.
Pour remédier à cela, les processeurs moden fournissent une sorte d'instructions CPU LOCK, UNLOCK pendant l'intervalle de désactivation du changement de contexte.
Sur Win32, utilisez InterlockedIncrement () pour faire i ++ pour la sécurité des threads. C'est beaucoup plus rapide que de compter sur le mutex.
la source
Si vous partagez même un int entre les threads dans un environnement multicœur, vous avez besoin de barrières de mémoire appropriées en place. Cela peut signifier utiliser des instructions imbriquées (voir InterlockedIncrement dans win32 par exemple), ou utiliser un langage (ou un compilateur) qui offre certaines garanties thread-safe. Avec la réorganisation des instructions au niveau du processeur, les caches et d'autres problèmes, à moins que vous n'ayez ces garanties, ne supposez pas que tout ce qui est partagé entre les threads est sûr.
Edit: Une chose que vous pouvez supposer avec la plupart des architectures est que si vous avez affaire à des mots simples correctement alignés, vous ne vous retrouverez pas avec un seul mot contenant une combinaison de deux valeurs qui ont été écrasées ensemble. Si deux écritures se superposent, l'une gagnera et l'autre sera rejetée. Si vous faites attention, vous pouvez en tirer parti et voir que ++ i ou i ++ sont thread-safe dans la situation à un seul écrivain / plusieurs lecteurs.
la source
Si vous voulez un incrément atomique en C ++, vous pouvez utiliser des bibliothèques C ++ 0x (le
std::atomic
type de données) ou quelque chose comme TBB.Il fut un temps où les directives de codage GNU disaient que la mise à jour des types de données qui correspondent à un mot était "généralement sûre", mais ce conseil est faux pour les machines SMP,
faux pour certaines architectureset faux lors de l'utilisation d'un compilateur d'optimisation.Pour clarifier le commentaire "mise à jour du type de données à un mot":
Il est possible que deux processeurs sur une machine SMP écrivent dans le même emplacement mémoire au cours du même cycle, puis tentent de propager la modification aux autres processeurs et au cache. Même si un seul mot de données est en cours d'écriture, de sorte que les écritures ne prennent qu'un seul cycle, elles se produisent également simultanément, vous ne pouvez donc pas garantir quelle écriture réussira. Vous n'obtiendrez pas de données partiellement mises à jour, mais une écriture disparaîtra car il n'y a pas d'autre moyen de gérer ce cas.
Compare-and-swap correctement les coordonnées entre plusieurs processeurs, mais il n'y a aucune raison de croire que chaque affectation de variable de types de données à un mot utilisera comparer et swap.
Et bien qu'un compilateur d'optimisation n'affecte pas la façon dont un chargement / stockage est compilé, il peut changer lorsque le chargement / stockage se produit, causant de sérieux problèmes si vous vous attendez à ce que vos lectures et vos écritures se produisent dans le même ordre qu'elles apparaissent dans le code source ( le plus connu étant le verrouillage à double vérification ne fonctionne pas en vanilla C ++).
REMARQUE Ma réponse initiale indiquait également que l'architecture Intel 64 bits était défectueuse dans le traitement des données 64 bits. Ce n'est pas vrai, j'ai donc modifié la réponse, mais ma modification affirmait que les puces PowerPC étaient cassées. Cela est vrai lors de la lecture de valeurs immédiates (c'est-à-dire des constantes) dans des registres (voir les deux sections intitulées "Chargement des pointeurs" sous le listing 2 et le listing 4). Mais il y a une instruction pour charger des données de la mémoire en un cycle (
lmw
), donc j'ai supprimé cette partie de ma réponse.la source
Sur x86 / Windows en C / C ++, vous ne devez pas supposer qu'il est thread-safe. Vous devez utiliser InterlockedIncrement () et InterlockedDecrement () si vous avez besoin d'opérations atomiques.
la source
Si votre langage de programmation ne dit rien sur les threads, mais s'exécute sur une plate-forme multithread, comment une construction de langage peut-elle être thread-safe?
Comme d'autres l'ont souligné: vous devez protéger tout accès multithread aux variables par des appels spécifiques à la plate-forme.
Il existe des bibliothèques qui font abstraction de la spécificité de la plate-forme, et le prochain standard C ++ a adapté son modèle de mémoire pour faire face aux threads (et peut donc garantir la sécurité des threads).
la source
Même s'il est réduit à une seule instruction d'assemblage, incrémentant la valeur directement en mémoire, il n'est toujours pas thread-safe.
Lors de l'incrémentation d'une valeur en mémoire, le matériel effectue une opération de «lecture-modification-écriture»: il lit la valeur de la mémoire, l'incrémente et la réécrit en mémoire. Le matériel x86 n'a aucun moyen d'incrémenter directement sur la mémoire; la RAM (et les caches) est uniquement capable de lire et de stocker des valeurs, pas de les modifier.
Supposons maintenant que vous ayez deux cœurs séparés, soit sur des sockets séparés, soit en partageant un seul socket (avec ou sans cache partagé). Le premier processeur lit la valeur et avant de pouvoir réécrire la valeur mise à jour, le second processeur la lit. Une fois que les deux processeurs ont réécrit la valeur, elle n'aura été incrémentée qu'une seule fois, pas deux fois.
Il existe un moyen d'éviter ce problème; Les processeurs x86 (et la plupart des processeurs multicœurs que vous trouverez) sont capables de détecter ce type de conflit dans le matériel et de le séquencer, de sorte que toute la séquence de lecture-modification-écriture semble atomique. Cependant, comme cela est très coûteux, cela ne se fait qu'à la demande du code, sur x86 généralement via le
LOCK
préfixe. D'autres architectures peuvent le faire par d'autres moyens, avec des résultats similaires; par exemple, les comparatifs et échanges atomiques liés à la charge / au stockage et à l'échange (les processeurs x86 récents ont également ce dernier).Notez que l'utilisation
volatile
n'aide pas ici; il indique seulement au compilateur que la variable a peut-être été modifiée en externe et que la lecture de cette variable ne doit pas être mise en cache dans un registre ou optimisée. Cela n'oblige pas le compilateur à utiliser des primitives atomiques.Le meilleur moyen est d'utiliser des primitives atomiques (si votre compilateur ou vos bibliothèques en ont), ou de faire l'incrémentation directement dans l'assembly (en utilisant les instructions atomiques correctes).
la source
Ne supposez jamais qu'un incrément se compilera en une opération atomique. Utilisez InterlockedIncrement ou toute autre fonction similaire existant sur votre plate-forme cible.
Edit: Je viens de rechercher cette question spécifique et l'incrémentation sur X86 est atomique sur les systèmes à processeur unique, mais pas sur les systèmes multiprocesseurs. L'utilisation du préfixe de verrouillage peut le rendre atomique, mais c'est beaucoup plus portable simplement d'utiliser InterlockedIncrement.
la source
Selon cette leçon d'assemblage sur x86, vous pouvez ajouter de manière atomique un registre à un emplacement mémoire , de sorte que votre code peut potentiellement exécuter de manière atomique '++ i' ou 'i ++'. Mais comme dit dans un autre article, le C ansi n'applique pas l'atomicité à l'opération '++', vous ne pouvez donc pas être sûr de ce que votre compilateur va générer.
la source
Le standard C ++ de 1998 n'a rien à dire sur les threads, bien que le prochain standard (attendu cette année ou la prochaine) le fasse. Par conséquent, vous ne pouvez rien dire d'intelligent sur la sécurité des threads des opérations sans faire référence à l'implémentation. Ce n'est pas seulement le processeur utilisé, mais la combinaison du compilateur, du système d'exploitation et du modèle de thread.
En l'absence de documentation contraire, je ne suppose pas que toute action est thread-safe, en particulier avec les processeurs multicœurs (ou les systèmes multiprocesseurs). Je ne ferais pas non plus confiance aux tests, car les problèmes de synchronisation des threads ne sont susceptibles de survenir que par accident.
Rien n'est thread-safe sauf si vous avez une documentation indiquant que c'est pour le système particulier que vous utilisez.
la source
Jetez i dans le stockage local des threads; ce n'est pas atomique, mais cela n'a pas d'importance.
la source
AFAIK, selon le standard C ++, les lectures / écritures dans un
int
sont atomiques.Cependant, tout ce que cela fait, c'est se débarrasser du comportement indéfini associé à une course aux données.
Mais il y aura toujours une course aux données si les deux threads tentent de s'incrémenter
i
.Imaginez le scénario suivant:
Soit d'
i = 0
abord:Thread A lit la valeur de la mémoire et stocke dans son propre cache. Le fil A incrémente la valeur de 1.
Le thread B lit la valeur de la mémoire et stocke dans son propre cache. Le fil B incrémente la valeur de 1.
Si tout cela est un seul thread, vous obtiendrez
i = 2
en mémoire.Mais avec les deux threads, chaque thread écrit ses modifications et donc Thread A réécrit
i = 1
en mémoire et Thread B écriti = 1
en mémoire.C'est bien défini, il n'y a pas de destruction ou de construction partielle ou de déchirement d'un objet, mais c'est toujours une course aux données.
Afin d'incrémenter de manière atomique,
i
vous pouvez utiliser:std::atomic<int>::fetch_add(1, std::memory_order_relaxed)
Un ordre assoupli peut être utilisé parce que nous ne nous soucions pas de l'endroit où cette opération a lieu, tout ce qui nous importe, c'est que l'opération d'incrémentation est atomique.
la source
Vous dites "ce n'est qu'une instruction, ce serait ininterrompu par un changement de contexte". - c'est bien beau pour un seul processeur, mais qu'en est-il d'un processeur double cœur? Ensuite, vous pouvez vraiment avoir deux threads accédant à la même variable en même temps sans aucun changement de contexte.
Sans connaître la langue, la réponse est d'en tester le diable.
la source
Je pense que si l'expression "i ++" est la seule dans une déclaration, elle équivaut à "++ i", le compilateur est assez intelligent pour ne pas garder une valeur temporelle, etc. Donc si vous pouvez les utiliser de manière interchangeable (sinon vous avez gagné ne demandez pas lequel utiliser), peu importe celui que vous utilisez car ils sont presque identiques (sauf pour l'esthétique).
Quoi qu'il en soit, même si l'opérateur d'incrémentation est atomique, cela ne garantit pas que le reste du calcul sera cohérent si vous n'utilisez pas les verrous corrects.
Si vous voulez expérimenter par vous-même, écrivez un programme où N threads incrémentent simultanément une variable partagée M fois chacun ... si la valeur est inférieure à N * M, alors un incrément a été écrasé. Essayez-le avec le pré-incrément et le post-incrément et dites-nous ;-)
la source
Pour un compteur, je recommande d'utiliser l'idiome compare and swap qui est à la fois non verrouillable et thread-safe.
Le voici en Java:
la source