I'm reading Anthony William's C++ Concurrency in Action. Chapter 7 describes the process of developing a lock-free stack and illustrates common issues that make lock-free programming difficult. Specifically, section 7.2.3 (Detecting nodes that can't be reclaimed using hazard pointers) describes how hazard pointers can be used to avoid a data race and make sure other threads don't delete a node still referenced by another thread.
This code is one of the iterations of pop() illustrated in that chapter:
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;
}
I have a doubt about this fragment:
if(outstanding_hazard_pointers_for(old_head))
{
reclaim_later(old_head);
}
else
{
delete old_head;
}
The purpose of the hazard pointers is making sure old_head is deleted when no other threads may still be using it. The suggested implementation of outstanding_hazard_pointers_for is the following:
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;
}
Basically, the array of hazard pointers is scanned to check whether the pointer to the node looked for is present. I'm wondering why this operation is indeed safe. An atomic load() is performed and even if sequentially consistent ordering is used, load() may load a stale value. As a consequence, p may not be found, and pop() would be deleting a node that is still in use.
Where's the flaw in this reasoning? I cannot find a justification as to why hp.store(old_head) on another thread will happen-before hazard_pointers[i].pointer.load().
Aucun commentaire:
Enregistrer un commentaire