I am reading the second edition of C++ Concurrency in Action. The code below is from Listing 7.6. It implements pop() for the stack using hazard pointers.
std::shared_ptr<T> pop() {
std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();
node* old_head = head.load(); // #1
do {
node* temp;
do {
temp = old_head;
hp.store(old_head); // #2
old_head = head.load(); // #3
} while (old_head != temp); // #4
} while (old_head &&
!head.compare_exchange_strong(old_head, old_head->next));
hp.store(nullptr);
// ...
}
The book explains the role of the inner loop:
You have to do this in a
whileloop to ensure that thenodehasn't been deleted between the reading of the oldheadpointer#1and the setting of the hazard pointer#2. During this window no other thread knows you're accessing this particular node. Fortunately, if the oldheadnode is going to be deleted,headitself must have changed, so you can check this and keep looping until you know that theheadpointer still has the same value you set your hazard pointer to#4.
According to the implementation of pop, if another thread deletes the head node by pop between #1 and #2, then head will be modified to the new node.
What confuses me is, can the modification of head by other threads be seen by the current thread in time? For example, if the new value of head has not been propagated to the current thread, then #1 and #3 will still read the same value (the old value), causing the inner while loop to exit and in turn the outer while loop accessing old_head->next, resulting in undefined behavior.
I've searched stackoverflow for some answers. For example, this answer says
The default memory ordering of
std::memory_order_seq_cstprovides a single global total order for allstd::memory_order_seq_cstoperations across all variables. This doesn't mean that you can't get stale values, but it does mean that the value you do get determines and is determined by where in this total order your operation lies.
This comment says
Every atomic variable has its own modification order that all threads can agree on, but that only serializes modifications, not reads. The coherency requirements involving reads just guarantee that if you've seen one value in the modification order, you can't see an earlier one.
But cppreference says
Each
memory_order_seq_cstoperationBthat loads from atomic variableM, observes one of the following:
- the result of the last operation
Athat modifiedM, which appears before B in the single total order...
So what exactly should the answer to my question be?
Also, if a weaker ordering (like release-acquire or relaxed) is used here, would the above problem arise?
Here is the code of pop:
std::shared_ptr<T> pop() {
std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();
node* old_head = head.load(); // #1
do {
node* temp;
do {
temp = old_head;
hp.store(old_head); // #2
old_head = head.load(); // #3
} while (old_head != temp); // #4
} while (old_head &&
!head.compare_exchange_strong(old_head, old_head->next));
hp.store(nullptr); // Clear hazard pointer once you're finished
std::shared_ptr<T> res;
if (old_head) {
res.swap(old_head->data);
if (outstanding_hazard_pointers_for(old_head)) // Check for hazard pointers referencing a node before you delete it.
reclaim_later(old_head);
else
delete old_head;
delete_nodes_with_no_hazards();
}
return res;
}
pop() pops the node pointed to by head and frees it when no hazard pointer points to it. Modifying the head is achieved by compare_exchange_strong.