C ++ 26: Lock-free stack simplified implementation

0
11
C ++ 26: Lock-free stack simplified implementation


According to the principle for lock-free programming in my previous post, it is now about practical implementation.

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



From the outside, the application is responsible for the protection of data. From the inside, the data structure is responsible for saving itself. A data structure that protects itself so that no data can be raced is called threadproof.

First of all, the question arises how to move forward in the design of a consistency data structure.

  • Locking strategy: Should the data structure support or have a thick or proper locking or lock-free? It is easy to apply coarse -second locking, but leads to conflict. A fine-dana or lock-free implementation is high demand. First of all: How can the thick -se -locking be understood? Coarse -Graz locking means that the data structure is used only by a thread at a certain time.
  • Interface granularity: The more powerful the interface of the thread-safe data structure, the more difficult it is to think about your use together.
  • Specific use pattern: If the readers mainly use your data structure, you should not adapt them to the authors.
  • Avoid flaws: Do not pass customers to the interior of their data structure.
  • Competition: Do customer inquiries often occur in your data structure?
  • Adiposity: If the number of customers increases simultaneously or the data structure is limited then what is the performance of your data structure?
  • Immovable: Which irreversible should be applied to your data structure when using?
  • Exception: What should happen if there is an exception?

Of course, these ideas depend on each other. For example, the use of a coarse-to-thin lock strategy can increase competition by data structure and loss scalability.

First of all: What does a stack look?

std::stack Follow the Lifo theory (already load). A pile staWho is the headerstack> Necessary, three members are functions: with sta.push(e)You can have a new element e Put the top of the stack, with it sta.pop() Remove the top and together sta.top() Refer to this. The stack comparison supports operators and knows its size.

#include 
...
std::stack myStack;

std::cout << myStack.empty() << '\n'; // true
std::cout << myStack.size() << '\n'; // 0

myStack.push(1);
myStack.push(2);
myStack.push(3);

std::cout << myStack.top() << '\n'; // 3
while (!myStack.empty()){
    std::cout << myStack.top() << " ";
    myStack.pop();
} // 3 2 1

std::cout << myStack.empty() << '\n'; // true
std::cout << myStack.size() << '\n'; // 0

Now let’s start applying the lock-free stack.

I start in my simplified implementation push-Member ceremony. First of all, I would like to explain how a new knot is added to a bus chain list. head Just the first knot in the chain list is indicative.







Just the chain list has two features in each knot: its value T And indicator next, next Just re -presents the next element to the chain list. Only refers to knot nullptrIt is straight to add a new node to the data. Create a new knot and let’s go nextStop the signal for the previous head. So far, the new knot is not accessible. Finally the new knot becomes a new head and closes it pushFrom -propos.

The following examples reflect the lock-free implementation of an concurrent pile:

// lockFreeStackPush.cpp

#include 
#include 

template
class LockFreeStackPush {
 private:
    struct Node {
        T data;
        Node* next;
        Node(T d): data(d), next(nullptr) {}
    };
    std::atomic head;
 public:
    LockFreeStackPush() = default;
    LockFreeStackPush(const LockFreeStackPush&) = delete;
    LockFreeStackPush& operator= (const LockFreeStackPush&)
      = delete;
   
    void push(T val) {
        Node* const newNode = new Node(val);                // 1
        newNode->next = head.load();                        // 2
        while( !head.compare_exchange_strong(newNode->next,
                                            newNode) );     // 3
    }
};
   
int main(){

    LockFreeStackPush lockFreeStack;
    lockFreeStack.push(5);
    
    LockFreeStackPush lockFreeStack2;
    lockFreeStack2.push(5.5);
    
    LockFreeStackPush<:string> lockFreeStack3;
    lockFreeStack3.push("hello");

}

I would like a decisive member function push Analyze it. This creates a new knot (1), moves its next indicator to the old head and converts the new knot into a new head in a so -called Cass operation (3). A cas operation provides comparisons and exchange operations in an atomic step.

Call newNode->next = head.load() Charges the old value of headWhen invited value newNode->next Still the same how head In (3), is head But newNode Update and call head.compare_exchange_strong Gives true Back. If not, it gives call false back and while-Cip is executed till the call true Return. head.compare_exchange_strong Gives false Back when another thread pair a new knot in the stack.

In the code example, lines (2) and (3) create a type of nuclear transactions. First a snapshot of the data structure is made (2), then an attempt to publish the transaction (3). If the snapshot is no longer valid, a rolling is done and efforts are made again.

Today’s simplified stack implementation serves me as the basis of future stacks.


(RME)

Most Gitops: Continuous delivery is good, progressive distribution is betterMost Gitops: Continuous delivery is good, progressive distribution is better

LEAVE A REPLY

Please enter your comment!
Please enter your name here