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 ++.
General idea
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?
A pile
std::stack
Follow the Lifo theory (already load). A pile sta
Who 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.
A simplified implementation
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 nullptr
It is straight to add a new node to the data. Create a new knot and let’s go next
Stop 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 push
From -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 head
When 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.
What will happen next?
Today’s simplified stack implementation serves me as the basis of future stacks.
(RME)
