我正试图找到无锁编程的脚。在阅读了有关内存排序语义的不同解释后,我想澄清可能发生的重新排序。据我所知,编译器可能会重新排序指令(由于程序编译时的优化)和CPU(运行时?)。
对于轻松的语义cpp reference,提供了以下示例:
// Thread 1:
r1 = y.load(memory_order_relaxed); // A
x.store(r1, memory_order_relaxed); // B
// Thread 2:
r2 = x.load(memory_order_relaxed); // C
y.store(42, memory_order_relaxed); // D
据说,当x和y最初为零时,代码被允许产生r1 == r2 == 42,因为尽管A在序列中排序 - 在线程1中的B之前并且C在线程2中的D之前排序,但没有任何阻止D在y的修改顺序中出现在A之前,而B出现在x的修改顺序中的C之前。怎么会发生这种情况?这是否意味着C和D被重新排序,因此执行顺序是DABC?是否允许重新排序A和B?
对于获取 - 释放语义,有以下示例代码:
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // never fires
assert(data == 42); // never fires
}
我想知道如果我们使用轻松的记忆顺序而不是获取?我想,可以在data
之前阅读p2 = ptr.load(std::memory_order_relaxed)
的值,但是p2
呢?
最后,为什么在这种情况下使用宽松的记忆顺序是好的?
template<typename T>
class stack
{
std::atomic<node<T>*> head;
public:
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
// put the current value of head into new_node->next
new_node->next = head.load(std::memory_order_relaxed);
// now make new_node the new head, but if the head
// is no longer what's stored in new_node->next
// (some other thread must have inserted a node just now)
// then put that new head into new_node->next and try again
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed))
; // the body of the loop is empty
}
};
我的意思是head.load(std::memory_order_relaxed)
和head.compare_exchange_weak(new_node->next, new_node, std::memory_order_release, std::memory_order_relaxed)
。
总结以上所有内容,我的问题基本上是什么时候我必须关心潜在的重新排序以及什么时候我不知道?
答案 0 :(得分:2)
对于#1,编译器可能会在从x加载(没有依赖项)之前向y发出存储,即使它没有,也可以在cpu / memory级别延迟x的加载。
对于#2,p2将是非零的,但* p2和数据都不一定具有有意义的值。
对于#3,只有一个发布此线程发布的非原子商店的行为,它是一个发布
你应该总是关心重新排序,或者更好的是,不要假设任何顺序:C ++和硬件都不是从上到下执行代码,它们只关注依赖关系。