A Lock-Free Stack: A Hazard Pointer Implementation Explained II

A Lock-Free Stack: A Hazard Pointer Implementation Explained II

In my last post, I started to explain a hazard pointer implementation: A Lock-Free Stack: A Hazard Pointer Implementation Explained I. Today, I will continue to explain the implementation.

The Retire List

The retire list has the public member functions isInUse, addNode, and deleteUnusedNodes. Additionally, it has the inner class RetireNode, an atomic member of it, and the private member function addToRetiredNodes.

template <typename T, Node MyNode = MyNode<T>>
class RetireList {

    struct RetiredNode {
        MyNode* node;
        RetiredNode* next;
        RetiredNode(MyNode* p) : node(p), next(nullptr) { }
        ~RetiredNode() {
            delete node;
        }
    };

    std::atomic<RetiredNode*> RetiredNodes;

    void addToRetiredNodes(RetiredNode* retiredNode) {
        retiredNode->next = RetiredNodes.load();
        while (!RetiredNodes.compare_exchange_strong(retiredNode->next, retiredNode));
    }

 public:

    bool isInUse(MyNode* node) {
        for (std::size_t i = 0; i < MaxHazardPointers; ++i) {
            if (HazardPointers<T>[i].pointer.load() == node) return true;
        }
        return false;
    }

    void addNode(MyNode* node) {
        addToRetiredNodes(new RetiredNode(node));
    }

    void deleteUnusedNodes() {
        RetiredNode* current = RetiredNodes.exchange(nullptr);
        while (current) {
            RetiredNode* const next = current->next;
            if (!isInUse(current->node)) delete current;
            else addToRetiredNodes(current);
            current = next;
        }
    }
};
        

Let’s start with the interface of the type RetireList.

The member function isInUse checks if node is in use. It does its job by traversing the variable template HazardPointers that is parameterized on the type of data the node holds. HazardPointers is a C-array of HazardPointer of length 50. A HazardPointer consists of an atomic thread id and an atomic pointer to a node.

constexpr std::size_t MaxHazardPointers = 50;

template <typename T, Node MyNode = MyNode<T>>
struct HazardPointer {
    std::atomic<std::thread::id> id;
    std::atomic<MyNode*> pointer;
};

template <typename T>
HazardPointer<T> HazardPointers[MaxHazardPointers];
        

Using an STL container such as std::set as HazardPointers is much more convenient. std::set is already ordered and guarantees constant access time on average but has a big issue: it’s not thread-safe.

The member function addNode takes a node, invokes the private member function addToRetiredNodes, and puts the node into an RetiredNode. RetiredNode is an RAII object and guarantees that the wrapped node is destroyed, releasing its memory. All retired nodes build a singly-linked list.

The member function deleteUnusedNodes traverses the singly-linked list of retired nodes by applying the following pattern:

    void deleteUnusedNodes() {
        RetiredNode* current = RetiredNodes.exchange(nullptr);
        while (current) {
            RetiredNode* const next = current->next;
            if (!isInUse(current->node)) delete current;
            else addToRetiredNodes(current);
            current = next;
        }
    }
        

It checks the current node, points the next node to current->next and updates the current node with the next node. Finally, the current node is destroyed if not used anymore or added to the retired nodes. The private member function addToRetireNodes adds the retired nodes to the singly-linked list. To perform its job, it loads the RetiredNodes and makes the new node retiredNode to the new head of the singly-linked list. Before retiredNode becoming the new head of the singly-linked list, I have to ensure that it is still the head of the singly-linked list because another thread could kick in and change the head of the singly-linked list in the meantime. Thanks to the while-loop, retiredNode becomes only the new head if retiredNode->next = RetiredNodes.load() holds. If not, retiredNode->next is updated to RetiredNodes.load().

There is only one piece of the puzzle left:

The Hazard Pointer Owner

template <typename T, Node MyNode = MyNode<T>>
class HazardPointerOwner {

    HazardPointer<T>* hazardPointer;

 public:
    HazardPointerOwner(HazardPointerOwner const &) = delete;
    HazardPointerOwner operator=(HazardPointerOwner const &) = delete;

    HazardPointerOwner() : hazardPointer(nullptr) {
        for (std::size_t i = 0; i < MaxHazardPointers; ++i) {
            std::thread::id old_id;
            if (HazardPointers<T>[i].id.compare_exchange_strong(
                                        old_id, std::this_thread::get_id())) {
                hazardPointer = &HazardPointers<T>[i];
                break;
            }
        }
        if (!hazardPointer) {
            throw std::out_of_range("No hazard pointers available!");
        }
    }

    std::atomic<MyNode*>& getPointer() {
        return hazardPointer->pointer;
    }

    ~HazardPointerOwner() {
        hazardPointer->pointer.store(nullptr);
        hazardPointer->id.store(std::thread::id());
    }
};
        

HazardPointerOwner holds a hazardPointer. This hazardPointer is set in the constructor by traversing all HazardPointers. The compare_exchange_strong call checks in an atomic step if the currently traversed hazard pointer is not set and sets its id to the id of the now executed thread (std::this_thread::get_id()). In the success case, hazardPointer becomes the new hazard pointer returned to the client invoking the member function getPointer. When all of the hazard pointers of HazardPointers are used, the constructor throws a std::out_of_range exception. Finally, HazardPointerOwner‘s destructor sets the hazardPointer to its default state.

 



Article content

Modernes C++ Mentoring

Do you want to stay informed: Subscribe.

 

What’s Next?

In C++26, we will get hazard pointers. I will write about them in my next post.




To view or add a comment, sign in

More articles by Rainer Grimm

  • Charity run for ALS

    Tomorrow, on the 28th, there will be a charity run for ALS in Rottenburg. The course is exactly 1 km long.

    2 Comments
  • Small Safety Improvements in the C++ 26 Core Language

    Safety is an important concern in C++26. Contracts are probably the most important feature for safety.

    1 Comment
  • My ALS Journey (30/n): Cippi at the CppCon

    This week was very exciting for Cippi. She visited CppCon in Aurora, near Denver.

    2 Comments
  • Contracts: Evaluation Semantic

    After briefly presenting the details of contracts in my last article, “Contracts: A Deep Dive“, I would like to take a…

  • My ALS Journey (29/n): I feel Good

    I often receive messages asking about my health and wishing me well. I am very happy to receive these messages and just…

    5 Comments
  • Contracts: A Deep Dive

    August 25, 2025/in C++26/by Rainer GrimmI already introduced contracts in the article “Contracts in C++26”. In this…

  • My ALS Journey (28/n): Bureaucracy – The German Disease

    Today I want to write about a sad topic. Bureaucracy in the German healthcare system is becoming increasingly absurd.

    2 Comments
  • Data-Parallel Types: Algorithms

    The data-parallel types library has four special algorithms for SIMD vectors. The four special algorithms are min, max,…

  • My ALS Journey (27/n): An Emergency Call

    Firstly, I would like to say that I am doing very well and have made a full recovery from my incident. However, I would…

    8 Comments
  • Data-Parallel Types: Reduction

    In this article, I will discuss reduction and mask reduction for data-parallel types. Reduction A reduction reduces the…

Others also viewed

Explore content categories