C ++ 26: A lock-free stack with dangerous sharp implementation

0
2
C ++ 26: A lock-free stack with dangerous sharp implementation


In the last posts I discussed the lock-free stack. Dangerous pointers solve all the problems of implementation shown in the previous contribution with the simple waste collector.

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 ++.

First, a note from my own behalf: In the end of 2023 I was diagnosed as a diagnosis, a serious progressive nerve disorder. Therefore, I will change the frequency with which I publish my blog for one to two weeks in the future. Writing an article in The Voice instead of typing is exceptionally tedious and time -consuming.

Word Hazard pointer back Magged M. Goes to MichaelDangerous signals solve the classic problem of lock-free data structures such as B. A lock-free stack: When a thread can safely remove a node of data structure, while other threads can use this knot at the same time?

(Image: Rainer Grim)

Although a dangerous indicator provides a general solution for the frequent problem of safe storage approval in lock-free data structures, I would like to show it from the point of view of my lock-free stack.



(Image: Rainer Grim)

A dangerous indicator is an indicator with a writing and access to many reading. All dangerous pointers make a linked list and are inscribed with a zero pointer. When a thread uses a stack node, the node address is inserted into a dangerous indicator, indicating that this thread uses this knot and is the only owner of the dangerous indicator used. If the thread no longer needs a knot, it holds a dangerous indicator on a zero indicator and thus leaves its own ownership.

A thread leads a list of dangerous points that stand for nodes used by thread and cannot be removed. If a thread wants to remove a knot, he discovers a list of all the dangers and checks what the knot is used. If the knot is not used, it will be removed. When the knot is used, it is finally placed in a decomitioning list of the nodes to be removed. After all, the knot is only added to the decomitioning list if it is not yet in the list.

This is with our lock-free stack. Member ceremony topAndPop There are two tasks regarding storage recovery. First, she manages the knot to be removed; Secondly, it goes through the declaration list of the node and removes them when they are no longer used.

I need the following member function in a new implementation topAndPopBased on previous details: getHazardPointerTo achieve reference to a dangerous indicator, retireList.addNode And retireList.deleteUnusedNodesFor a knot for retireList Add. moreover retireList.deleteUnusedNodesTo remove all the nodes that are no longer used from the selection list. In addition, the member uses function retireList.deleteUnusedNode Auxiliary work retireList.isInUseTo decide whether a knot is currently being used. Member ceremony isInUse Is also in topAndPop It is useful to decide whether the current knot should be added to the selection list or directly removed.

How does this affect my previous implementation? Let’s see. The following program reflects the implementation of a lock-free stack based on dangerous points:

// lockFreeStackHazardPointers.cpp

#include 
#include 
#include 
#include 
#include 
#include 

template 
concept Node = requires(T a) {
{T::data};
{ *a.next } -> std::same_as;
};

template 
struct MyNode {
T data;
MyNode* next;
MyNode(T d): data(d), next(nullptr){ }
};

constexpr std::size_t MaxHazardPointers = 50;

template >
struct HazardPointer {
std::atomic<:thread::id> id;
std::atomic pointer;
};

template 
HazardPointer HazardPointers(MaxHazardPointers);

template >
class HazardPointerOwner {

HazardPointer* 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(i).id.compare_exchange_strong(
                                        old_id, std::this_thread::get_id())) {
hazardPointer = &HazardPointers(i);
break;
}
}
if (!hazardPointer) {
throw std::out_of_range(„No hazard pointers available!“);
}
}

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

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

Template >
std::atomic& getHazardPointer() {
    thread_local static HazardPointerOwner hazard;
return hazard.getPointer();
}

template >
class RetireList {

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

std::atomic 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(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;
}
}

};

template>
class LockFreeStack {

std::atomic head;
RetireList retireList;
 
public:
LockFreeStack() = default;
LockFreeStack(const LockFreeStack&) = delete;
    LockFreeStack& operator= (const LockFreeStack&) = delete;
   
  void push(T val) {
  MyNode* const newMyNode = new MyNode(val);
newMyNode->next = head.load();
while( !head.compare_exchange_strong(newMyNode->next, newMyNode) );
}

T topAndPop() {
        std::atomic& hazardPointer = getHazardPointer();
MyNode* oldHead = head.load();
do {
MyNode* tempMyNode; 
do {
tempMyNode = oldHead;
hazardPointer.store(oldHead);
oldHead = head.load();
            } while( oldHead != tempMyNode );
} while( oldHead && !head.compare_exchange_strong(oldHead, oldHead->next) ) ;
if ( !oldHead ) throw std::out_of_range(„The stack is empty!“);
hazardPointer.store(nullptr);
auto res = oldHead->data;
        if ( retireList.isInUse(oldHead) ) retireList.addNode(oldHead);
else delete oldHead;
retireList.deleteUnusedNodes();
return res;
}
};

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 program runs expected:



(Image: Screenshot (Rainer Grim))

I will analyze the phase rate in my next post.


(RME)

Mobile phone is checked 58 times every hour before bedMobile phone is checked 58 times every hour before bed

LEAVE A REPLY

Please enter your comment!
Please enter your name here