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 topAndPop
In 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 copyOfToBeDeletedNodes
To remove the list of all the nodes removed (9). If another topAndPop
-Ccification is pushed into the middle, I use the local indicator copyOfToBeDeletedNodes
around toBeDeletedNodes
To 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_strong
To 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.
What will happen next?
With RCU and Hazard pointers, problems can be solved in C ++ 26.
(RME)