I feel like there could about the a dangling pointer in this example implementation of a lock-free stack in C++: Concurrency in Action. The author, Anthony Williams, provides a beginning for the lock-free stack class as:
class LockFreeStack {
private:
struct Node {
T data;
Node* next;
Node(T const& data_) : data(data_) {}
};
std::atomic<Node*> head;
public:
void push(T const& data) {
Node* const new_node = new Node(data);
new_node->next = head.load();
while(!head.compare_exchange_weak(new_node->next, new_node));
}
};
Williams then discusses how to implement pop. He says, "push() doesn’t touch the node once it’s been added to the stack, so the thread that called pop() must be the only thread that can touch the node, and it can safely delete it." Nodes to delete are initially added on to a std::atomic<Node*> pending_deletion_head, and a variable std::atomic<unsigned> threads_in_pop stores the count of threads that are accessing pop. The entire chain extending from pending_deletion_head is finally deleted when threads_in_pop is zero.
This makes sense to me in general, but I'm confused about how push is safe. Specifically, couldn't new_node->next in the line while(!head.compare_exchange_weak(new_node->next, new_node)); be a dangling pointer with the given implementation of pop?
The implementation of
pushis safe because you only accessnew_node->next.new_nodeis always locally created withnew Node, so it cannot be a dangling pointer. The statement:... is effectively doing:
Accessing
new_node->nextis always safe becausenew_nodeis the new node you've created earlier.new_node->nextmight be replaced withheadduring the loop, which makes it an invalid pointer, butnew_nodeitself is never dangling, which makes this okay.