C ++ 26: A simple garbage collector for lock-free stack

0
8
C ++ 26: A simple garbage collector for lock-free stack


In the last three articles, I have dealt with the subject of lock-free stack:

Advertisement





Rainer Grim has been working as a software architect, team and training manager for many years. He likes to write articles on programming languages ​​C ++, Python and Haskel, but also likes to speak at expert conferences. On his blog modern C ++, he deal intensively with his passion C ++.

For this post, I extend it with a simple garbage collector.

I have discussed that more than one execution topAndPush-Lal is a race position. I can safely remove a knot if not more than one topAndPush-Lal is done at the same time. This observation is important to solve the memory leak problem: I save distant nodes in a list to remove and remove nodes in this list, if not more than one topAndPush-Coling is active. Only one challenge: How can I make sure that nothing more than one topAndPush-The cooling is active? I initially use an atomic counter topAndPush Finally extended and reduced. The counter is zero or one if not or not topAndPush-Coling is active.

The following program implements the strategy presented. I use the file lockFreeStackWithLeaks.cpp The article “C ++ 26: Full Implementation of a Lock-Free Stack as a starting point”.

// lockFreeStackWithGarbageCollection.cpp

#include 
#include 
#include 
#include 
#include 

template
class LockFreeStack {
 private:
    struct Node {
        T data;
        Node* next;
        Node(T d): data(d), next(nullptr){ }
    };

    std::atomic head{nullptr};
    std::atomic topAndPopCounter{};                                            // 1
    std::atomic toBeDeletedNodes{nullptr};                                   // 2

    void tryToDelete(Node* oldHead) {                                              // 3
        if (topAndPopCounter  == 1) {                                              // 6
            Node* copyOfToBeDeletedNodes = toBeDeletedNodes.exchange(nullptr);     // 8
            if (topAndPopCounter == 1) deleteAllNodes(copyOfToBeDeletedNodes);     // 9
            else addNodeToBeDeletedNodes(copyOfToBeDeletedNodes); 
            delete oldHead;
        }
        else addNodeToBeDeletedNodes(oldHead);                                     // 7
    }

    void addNodeToBeDeletedNodes(Node* oldHead) { 
        oldHead->next = toBeDeletedNodes;
        while( !toBeDeletedNodes.compare_exchange_strong(oldHead->next, oldHead) ); // 10
    }

    void deleteAllNodes(Node* currentNode) {                                      // 4
        while (currentNode) {
            Node* nextNode = currentNode->next;
            delete currentNode;
            currentNode = nextNode;
        }
     }

 public:
    LockFreeStack() = default;
    LockFreeStack(const LockFreeStack&) = delete;
    LockFreeStack& operator= (const LockFreeStack&) = delete;
   
    void push(T val) {
        Node* const newNode = new Node(val); 
        newNode->next = head.load();
        while( !head.compare_exchange_strong(newNode->next, newNode) );
    }

    T topAndPop() {
        ++topAndPopCounter;
        Node* oldHead = head.load();
        while( oldHead && !head.compare_exchange_strong(oldHead, oldHead->next) ) {
            if ( !oldHead ) throw std::out_of_range("The stack is empty!");
        }
        auto topElement = oldHead->data;
        tryToDelete(oldHead);                                                   // 5
        --topAndPopCounter;
        return topElement;
    }
};
   
int main(){

    LockFreeStack lockFreeStack;
    
    auto fut = std::async((&lockFreeStack){ lockFreeStack.push(2011); });
    auto fut1 = std::async((&lockFreeStack){ lockFreeStack.push(2014); });
    auto fut2 = std::async((&lockFreeStack){ lockFreeStack.push(2017); });
    
    auto fut3 = std::async((&lockFreeStack){ return lockFreeStack.topAndPop(); });
    auto fut4 = std::async((&lockFreeStack){ return lockFreeStack.topAndPop(); });
    auto fut5 = std::async((&lockFreeStack){ return lockFreeStack.topAndPop(); });
    
    fut.get(), fut1.get(), fut2.get();
    
    std::cout << fut3.get() << '\n';
    std::cout << fut4.get() << '\n';
    std::cout << fut5.get() << '\n';

}

The lock-free stack has two new features and three new member functions. Nuclear counter topAndPopCounter Counting (1) Number of active topAndPop-Crepfen, and atomic indicator toBeDeletedNodes (2) There is an indicator in the list of removed nodes. Member also tries tryToDelete (3), remove the knots. Member ceremony addNodeToBeDeletedNodes Adds a node to the list of deleted nodes, and member function deleteAllNodes (4) Removes all nodes.

Now we analyze the member function topAndPopIn the beginning and the end topAndPop Becomes topAndPopCounter Increased and reduced. oldHead The stack is removed and therefore can be called finally Trytodlete (5) To be removed. Member ceremony tryToDelete First check whether one or more topAndPop-The colors are active. If one topAndPop-Coling is active (6), becomes oldHead is removed. If not, it becomes oldHead Added to the list of deleted entries (7). I think only one topAndPop-Coling is active. In this case I make a local indicator copyOfToBeDeletedNodes On the nodes to be removed and insert toBeDeletedNodes-Pointer on one nullptr ((). Before I remove the knots, I check that there is no one else topAndPop-Coling is active. If present topAndPop I use local indicator copyOfToBeDeletedNodesTo remove the list of all the nodes removed (9). If another topAndPop-Ccification is pushed into the middle, I use the local indicator copyOfToBeDeletedNodesaround toBeDeletedNodesTo update the pointers.

Two assistants addNodeToBeDeletedNodes And deleteAllNodes Ittery through the list. deleteAllNodes Only when called A topAndPop-Coling is active. As a result, no synchronization is required. This statement does not apply to member function addNodeToBeDeletedNodes (9) and (7). It should be synchronized because more than one topAndPop-The colors can be active. while-E loop does this oldHead Removed the first knot in the knot and used one compare_exchange_strongTo keep this fact in mind topAndPop-Con overlap. Overlapping topAndPop-Tols lead to the fact that oldHead->next != toBeDeletedNodes (Line 10) and oldHead->next But toBeDeletedNodes Should be updated.



So far, this lock-free stack implementation works expected, but there are some weaknesses. If many topAndPop-CrePF call, it can be counter topAndPopCounter Never becomes one. This means that lumps are not removed in the lists of the removed nodes, and we have a memory leak. In addition, the number of removed nodes can become a resource problem.

With RCU and Hazard pointers, problems can be solved in C ++ 26.


(RME)

Gitlab 17.9 AI allows self-hosting for auxiliary gitlab pair chatGitlab 17.9 AI allows self-hosting for auxiliary gitlab pair chat

LEAVE A REPLY

Please enter your comment!
Please enter your name here