According to the principle for lock-free programming in the previous article, the full implementation of the lock-free stack has now been followed.
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 ++.
Sequential stability
This paragraph is optional. In my examples I use standard memory regulations: Sequential stabilityThe reason for this is simple. The sequential stability provides the strongest guarantee of all memory rules and is therefore easier to use compared to other storage rules. The sequential stability is an ideal starting point for the design of lock-free data structures. In further adaptation stages, memory rules can be weakened and A Received release semantha Or Comfortable semantics used.
Depending on the architecture, it may be that a weakness of the memory system does not pay. For example, the X86 memory model is one of the strongest memory models of all more modern architecture. Therefore, the cancellation of sequential stability and the use of a weak memory order cannot improve desired performance. In contrast, Armv8, Powerpc, Itanium and especially Dec Alpha can pay if the sequential stability is canceled.
The simplified stack version of the previous article has two problems: first, it does not have a bridge operation and second, it does not release any memory.
A complete implementation
A stack member usually supports the functions push
, pop
And top
Implementation of members works pop
And top
In a threadproof manner, it does not guarantee from the call top
After pop
thread safe. It may happen that a thread t1 stack.top()
Call and other threads t2
Overlade, stack.top()
And then stack.pop()
Call. Last call now pop
On the wrong stack size.
no storage
As a result, two elements functions are in the following implementation top
And pop
Combine in the same function: topAndPop
,
// lockFreeStackWithLeaks.cpp
#include
#include
#include
#include
template
class LockFreeStack {
private:
struct Node {
T data;
Node* next;
Node(T d): data(d), next(nullptr){ }
};
std::atomic head;
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() {
Node* oldHead = head.load(); // 1
while( oldHead && !head.compare_exchange_strong(oldHead, oldHead->next) ) { // 2
if ( !oldHead ) throw std::out_of_range("The stack is empty!"); // 3
}
return oldHead->data; // 4
}
};
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(); // 5
std::cout << fut3.get() << '\n';
std::cout << fut4.get() << '\n';
std::cout << fut5.get() << '\n';
}
Member ceremony topAndPop
Gives back the top element of the stack. She reads the head element of the stack (line 1) and creates a new head to the next knot oldHead
No nullptr
Is (line 2). oldhead
One is nullptr
When the stack is empty. I throw an exception when the stack is empty (line 3). A special non-value refund or return of A std::optional
There is also a valid option. Popping value causes a disadvantage: if there is an exception like a copy constructor std::bad_alloc
The trigger, the value is lost. Finally, the member returns the function head element (line 4).
Call fut.get()
, fut1.get()
, fut2.get()
(Row 5) Make sure that the related promise is made. If you do not give the launch policy, the promise can delay the collar thread. Lazy means that the promise is made only when the future get
Or wait
Asks for its result. Promise can also begin in a different thread:
auto fut = std::async(std::launch::asnyc, (&conStack){ conStack.push(2011); });
auto fut1 = std::async(std::launch::asnyc, (&conStack){ conStack.push(2014); });
auto fut2 = std::async(std::launch::asnyc, (&conStack){ conStack.push(2017); });
The output of the program provides at the end:
Although the lock-free stack launched push
And topAndPop
Supported, he has a serious problem: he loses memory. Why you can oldHead
Not just after calling head.compare_exchange_strong(oldHead, oldHead->next)
(2) In member ceremony topAndPop
be removed? The answer is another thread oldHead
Can use. We analyze the functions of the members push
And topAndPop
Extracts of simultaneous push
No problem because call !head.compare_exchange_strong(newNode->next, newNode)
newNode->next
The atom was updated for new heads. This also applies if only one call topAndPop
takes place. The problem occurs when many thoughts topAndPop
With or without call push
Are nested. Is being removed oldHead
While another thread uses it, it will be frightening because extinguishing oldHead
Before or after its update should always be done on new heads: oldHead->next
(2).
What will happen next?
Thanks to RCU and Hazard Points, I can remove memory loss in my next article.
(RME)