J'essaie de répondre à cette question moi-même, après avoir parcouru diverses ressources en ligne (par ex, celui-ci y celui-ci ), la norme C++11, ainsi que les réponses données ici.
Les questions connexes sont fusionnées (par exemple, " pourquoi !attendu ? " est fusionné avec "pourquoi mettre compare_exchange_weak() dans une boucle ? ") et les réponses sont données en conséquence.
Pourquoi compare_exchange_weak() doit-il être dans une boucle dans presque toutes les utilisations ?
Modèle typique A
Vous devez réaliser une mise à jour atomique basée sur la valeur de la variable atomique. Un échec indique que la variable n'est pas mise à jour avec la valeur souhaitée et que nous voulons réessayer. Notez que nous ne nous soucions pas vraiment de savoir s'il échoue en raison d'une écriture simultanée ou d'un échec fallacieux. Mais nous nous soucions que c'est nous qui font ce changement.
expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
Un exemple concret est l'ajout simultané par plusieurs threads d'un élément à une liste singulièrement liée. Chaque thread charge d'abord le pointeur de tête, alloue un nouveau nœud et ajoute la tête à ce nouveau nœud. Enfin, il essaie d'échanger le nouveau nœud avec la tête.
Un autre exemple consiste à mettre en œuvre un mutex en utilisant std::atomic<bool>
. Un seul thread au maximum peut entrer dans la section critique à la fois, en fonction du thread qui a défini en premier la section critique. current
a true
et sortir de la boucle.
Modèle typique B
Il s'agit en fait du modèle mentionné dans le livre d'Anthony. Contrairement au modèle A, vous voulez que la variable atomique soit mise à jour une fois, mais vous ne vous souciez pas de savoir qui le fait. Tant qu'il n'est pas mis à jour, vous réessayez. Ceci est typiquement utilisé avec des variables booléennes. Par exemple, vous devez implémenter un déclencheur pour qu'une machine à états passe à l'action. Le thread qui déclenche le déclencheur est indépendant.
expected = false;
// !expected: if expected is set to true by another thread, it's done!
// Otherwise, it fails spuriously and we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);
Notez que nous ne pouvons généralement pas utiliser ce modèle pour implémenter un mutex. Sinon, plusieurs threads peuvent se trouver à l'intérieur de la section critique en même temps.
Cela dit, il devrait être rare d'utiliser compare_exchange_weak()
en dehors d'une boucle. Au contraire, il y a des cas où la version forte est utilisée. Par exemple,
bool criticalSection_tryEnter(lock)
{
bool flag = false;
return lock.compare_exchange_strong(flag, true);
}
compare_exchange_weak
n'est pas approprié ici parce que lorsqu'il revient à cause d'un échec fallacieux, il est probable que personne n'occupe encore la section critique.
Starving Thread ?
Un point qui mérite d'être mentionné est ce qui se passe si les échecs fallacieux continuent à se produire, ce qui affame le fil de discussion ? Théoriquement, cela pourrait se produire sur des plateformes où compare_exchange_XXX()
est implémenté comme une séquence d'instructions (par exemple, LL/SC). Des accès fréquents à la même ligne de cache entre LL et SC produiront des défaillances parasites continues. Un exemple plus réaliste est dû à un ordonnancement stupide où tous les threads concurrents sont entrelacés de la manière suivante.
Time
| thread 1 (LL)
| thread 2 (LL)
| thread 1 (compare, SC), fails spuriously due to thread 2's LL
| thread 1 (LL)
| thread 2 (compare, SC), fails spuriously due to thread 1's LL
| thread 2 (LL)
v ..
Cela peut-il arriver ?
Heureusement, ce ne sera pas toujours le cas, grâce aux exigences du C++11 :
Les implémentations doivent garantir que les opérations de comparaison et d'échange faibles ne renvoient pas systématiquement false, sauf si l'objet atomique a une valeur différente de celle attendue ou qu'il y ait des modifications modifications simultanées de l'objet atomique.
Pourquoi prendre la peine d'utiliser compare_exchange_weak() et d'écrire la boucle nous-mêmes ? Nous pouvons simplement utiliser compare_exchange_strong().
Ça dépend.
Cas 1 : Quand les deux doivent être utilisés à l'intérieur d'une boucle. C++11 dit :
Lorsqu'une comparaison et échange est dans une boucle, la version faible donnera meilleures performances sur certaines plateformes.
Sur x86 (au moins actuellement. Peut-être qu'un jour, un schéma similaire à celui de LL/SC sera utilisé pour améliorer les performances lorsque davantage de cœurs seront introduits), les versions faible et forte sont essentiellement les mêmes car elles se résument toutes deux à une seule instruction. cmpxchg
. Sur certaines autres plateformes où compare_exchange_XXX()
n'est pas mis en œuvre atomiquement (ce qui signifie ici qu'il n'existe pas de primitive matérielle unique), la version faible à l'intérieur de la boucle peut gagner la bataille parce que la version forte devra gérer les faux échecs et réessayer en conséquence.
Mais,
rarement, on peut préférer compare_exchange_strong()
sur compare_exchange_weak()
même dans une boucle. Par exemple, lorsqu'il y a beaucoup de choses à faire entre le chargement d'une variable atomique et l'échange d'une nouvelle valeur calculée (cf. function()
ci-dessus). Si la variable atomique elle-même ne change pas fréquemment, nous n'avons pas besoin de répéter le calcul coûteux pour chaque échec fallacieux. Au lieu de cela, nous pouvons espérer que compare_exchange_strong()
"absorber" de tels échecs et nous ne répétons le calcul que lorsqu'il échoue en raison d'un véritable changement de valeur.
Cas 2 : Lorsque seulement compare_exchange_weak()
doivent être utilisés à l'intérieur d'une boucle. C++11 dit aussi :
Lorsqu'une comparaison et un échange faibles nécessitent une boucle et qu'une comparaison et un échange forts n'en ont pas besoin. ne le ferait pas, la comparaison forte est préférable.
C'est typiquement le cas lorsque vous bouclez juste pour éliminer les échecs parasites de la version faible. Vous réessayez jusqu'à ce que l'échange soit réussi ou échoue à cause d'une écriture concurrente.
expected = false;
// !expected: if it fails spuriously, we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);
Au mieux, c'est réinventer les roues et faire la même chose que compare_exchange_strong()
. Pire ? Cette approche ne permet pas de tirer pleinement parti des machines qui fournissent des comparaisons et des échanges non fallacieux dans le matériel. .
Enfin, si vous bouclez pour d'autres choses (par exemple, voir le "schéma type A" ci-dessus), il y a de fortes chances que compare_exchange_strong()
doit également être placé dans une boucle, ce qui nous ramène au cas précédent.
5 votes
En ce qui concerne la première question, dans de nombreux cas, vous devez quand même boucler (que vous utilisiez la version forte ou faible), et la version faible peut avoir de meilleures performances que la version forte.
3 votes
Le CAS faible et le CAS fort sont tous deux implémentés "à l'aide de LL/SC", de la même manière que le tri à bulles et le tri rapide sont implémentés "à l'aide de swap", c'est-à-dire dans le sens où il s'agit de l'opération primitive utilisée pour accomplir la tâche. Ce qu'ils recouvrent autour de LL/SC est très différent. Le CAS faible est juste LL/SC. Le CAS fort est LL/SC avec un tas d'autres choses.
1 votes
forums.manning.com/posts/list/33062.page Est-ce que ça aide ?
0 votes
@TuXiaomi avec la réponse dans ce lien, je ne vois pas pourquoi "la version faible donnera de meilleures performances sur certaines plateformes" comme indiqué dans la norme.
0 votes
@Deqing Sur d'autres, compare_exchange_weak peut échouer de manière fallacieuse, en raison d'interruptions ou d'actions d'autres processeurs ou threads. Sur ces plates-formes, compare_exchange_strong est effectivement une boucle sur compare_exchange_weak - s'il a échoué de manière fallacieuse, il boucle à nouveau. Est-ce que cela aide ? Je me trompe peut-être