Lock-free stack in C ++ 26: Details on implementation of dangerous pointers

0
2
Lock-free stack in C ++ 26: Details on implementation of dangerous pointers


In my previous post I presented the implementation of dangerous pointers: a lock-free stack that has a dangerous-pointer implementation. Today I will explain the implementation.MyNode There is a square template that is parameter by data type: which includes: data, MyNode Model the concept Node,




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

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){ }
};

Concepts are predicted during compilation time. They define semantic restrictions for template parameters. concept Node Is necessary data-Member and a indicator nextOne Node Return.

(Image: Rainer Grim)

The data types of lockfreestackhazardpointers.cPP program are essentially on it data-Member and Concept Node Parameter. MyNode Model the concept NodeHere, for example, announcement LockFreeStack,

template>

class LockFreeStack;

This is what the program looks with a lock-free stack:

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;
}
};

Call push It is not important from an approach, there, there head A atomic step is updated. Also, the call guarantees compare_exchange_strongHe head Always the current head of the stack.

Due to dangerous points, call will be called topAndPop More complex. First, the function refers to getHazardPointer On the dangerous indicator for current thread. Call hazardPointer.store(oldHead) Current threads owner of danger indicator, and calls hazardPointer.store(nullptr) Leaves its possession.

First I analyze the internal and external do-white loops. The inner loop holds a dangerous indicator on the stack’s head. When the following is implemented, the Do-Whil Loop ends: oldHead == tempNode,

Both are the same miles the same oldHead The current is still the current of the lock-free stack. oldHead Was set and now the current head cannot be, because a separate thread can come in and oldHead Already managed.

The outer do-whong is similar to the previous implementation of the lock-free stack. I am in the loop with me compare_exchange_strong And put on the head oldHead->nextFinally Head Stack head. Memberfunktion topAndPop The head value should be returned and removed. Before me oldHead Use, I have to check what oldHead There is no zero indicator. If this is the case, I throw an exception.

Rest of topAndPop Is simple. Call retireList.isInUse(oldHead) Checked what oldHead Is still used. This depends on the result of the review oldHead Selection list retireList.addNode It is added if it is not yet in the list or has been removed. Final call retireList.deleteUnusedNodes Member function is the most labor -confused call topAndPopMember ceremony retireListe.deleteUnusedNodes Goes through the whole Retire-List and removes all nodes that are no longer used.

For the reasons for the performance, the call should be made retireList.deleteUnusedNodes Every time you don’t call topAndPop This is a better strategy, member function deleteUnusedNodes Call when you have length Retire-List exceeds a certain range. Length of agar Retire-List, for example, doubled to double the stack, at least half of the knots can be removed. This is a compromise between requirements for limit performance and storage consumption.

Work here getHazardPointer,

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

The function refers to a dangerous indicator using the owner hazardA thread restaurant and stable variable. Therefore, each thread receives his copy of the threat indicator owner and his lifetime is bound for the lifetime of his own thread. The bound lifetime of the owner of the Hazard pointer is of significant importance, as it is guaranteed to be removed by dangerous pointers if the thread restaurant owners of the dangerous pointer are destroyed. I write more about this Raii object in my analysis of data types HazardPointerOwner,

In my next article I will explain the remaining implementation.


(RME)

IOS 19 and CO .: Apple announced WWDC for 9 JuneIOS 19 and CO .: Apple announced WWDC for 9 June

LEAVE A REPLY

Please enter your comment!
Please enter your name here