Je suis en train de lire les d'Anthony William en simultané en action. Le chapitre 7 décrit le processus de développement d'une pile sans verrou et illustre les problèmes courants qui rendent la programmation sans verrous difficile. Plus précisément, la section 7.2.3 (Détecter les nœuds qui ne peuvent pas être récupérés à l'aide des pointeurs de danger) décrit comment les pointeurs de danger peuvent être utilisés pour éviter une course de données et s'assurer que les autres threads ne sont pas référencés par un autre thread.Pile sans verrouillage: problème de visibilité lors de la vérification des indicateurs de danger pendant le bruit()?
Ce code est l'une des itérations de pop()
illustrés dans ce chapitre:
std::shared_ptr<T> pop()
{
std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();
node* old_head = head.load();
do
{
node* temp;
do
{
temp = old_head;
hp.store(old_head);
old_head = head.load();
} while(old_head != temp);
}
while(old_head &&
!head.compare_exchange_strong(old_head,old_head->next));
hp.store(nullptr);
std::shared_ptr<T> res;
if(old_head)
{
res.swap(old_head->data);
if(outstanding_hazard_pointers_for(old_head))
{
reclaim_later(old_head);
}
else
{
delete old_head;
}
delete_nodes_with_no_hazards();
}
return res;
}
J'ai un doute sur ce fragment:
if(outstanding_hazard_pointers_for(old_head))
{
reclaim_later(old_head);
}
else
{
delete old_head;
}
Le but des pointeurs de danger est de se assurer old_head
est supprimé lorsque aucun autre thread ne l'utilise encore. La mise en œuvre proposée de outstanding_hazard_pointers_for
est la suivante:
unsigned const max_hazard_pointers=100;
struct hazard_pointer
{
std::atomic<std::thread::id> id;
std::atomic<void*> pointer;
};
hazard_pointer hazard_pointers[max_hazard_pointers];
bool outstanding_hazard_pointers_for(void* p)
{
for(unsigned i=0; i < max_hazard_pointers; ++i)
{
if(hazard_pointers[i].pointer.load() == p)
{
return true;
}
}
return false;
}
Fondamentalement, le tableau de pointeurs de risque est analysé pour vérifier si le pointeur vers le noeud recherché est présent. Je me demande pourquoi cette opération est sûre. Un load()
atomique est effectué et même si l'ordre séquentiellement cohérent est utilisé, load()
peut charger une valeur périmée. En conséquence, p
peut ne pas être trouvé, et pop()
supprimerait un nœud qui est toujours utilisé.
Imaginez ce qui suit se produit:
thread A commence à exécuter
pop()
et est préempté juste avant d'exécuter:while(old_head && !head.compare_exchange_strong(old_head,old_head->next));
Discussion A voit donc la tête actuelle comme
old_head
, qui est enregistré dans son indicateur de danger.old_head
sera déréférencé lorsque le thread se réveille et tente de faire apparaître la têtehead.compare_exchange_strong(old_head, old_head->next)
.Fil B commence l'invocation
pop()
jusqu'àif(outstanding_hazard_pointers_for(old_head))
old_head
sera la tête courant de la pile, qui est le même nœud que le fil A fait référence en tant queold_head
. Discussion B pasdelete old_head
ssi aload()
sur le pointeur de danger de fil Renvoie la dernière valeur stockée par fil A.
En gros: Je me demande si le thread B peut load()
une valeur rassis au lieu de la plus récente . Dit d'une autre manière, je ne sais pas pourquoi a pour retourner la valeur définie par Thread A (old_node
).
Où est la faille dans ce raisonnement? Je ne peux pas trouver une justification pour savoir pourquoi hp.store(old_head)
sur un autre thread arrivera avant hazard_pointers[i].pointer.load()
.
Je lis le livre et j'ai exactement la même question que vous. Je ne comprends pas comment la réponse ci-dessous répond à vos préoccupations. Puisque vous avez eu le même problème de compréhension, pourriez-vous reformuler la raison pour laquelle le scénario que vous décrivez ne peut pas se produire et nous avons un "avant" entre le balayage de la liste des dangers et le magasin avant le CAS? – JJ15k
JJ15k, je viens de répondre à votre question. –