【发布时间】:2017-02-28 20:25:36
【问题描述】:
我正在尝试在无锁编程中立足。在阅读了关于内存排序语义的不同解释后,我想澄清一下可能发生的重新排序。据我了解,编译器(由于编译程序时的优化)和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)。
总结以上所有内容,我的问题本质上是我什么时候需要关心潜在的重新排序,什么时候不需要?
【问题讨论】:
-
请注意,如果
int data在没有同步保证的情况下被读取,那么 #2 将根据 C++ 标准具有未定义的行为 - 保证会在仍在写入时阻止读取线程查看它。那将是 UB 类型的数据竞赛,因为它不是 std::atomic 类型。std::atomic<int>使用memory_order_relaxed将避免 UBdata,并且只为您留下一个有效(但不太有用)的程序,其中包含一个普通数据竞争错误。但是取消引用ptr后的字符串比较仍然是数据竞争,因为std::string不是原子/无锁容器。 -
@PeterCordes 我真的不明白为什么字符串比较会成为数据竞赛。在消费者中,我们一直等到
p2被初始化。然后我们比较字符串。由于“不要破坏单线程代码”规则,必须保留此顺序。一旦我们在p2中得到了一些东西,我们就可以保证看到指向的数据(正如你提到的,在除 Alpha 之外的所有 CPU 上)。数据必须至少和指针一样新,因此如果指针被初始化,数据也必须已经被初始化。还是我误会了你? -
我的意思是,如果作者或读者使用
memory_order_relaxed,这是一场数据竞争,因为其中任何一个都允许指针读取为非 NULL,而std::string类中的数据仍在书面。 (请注意,对于非 Alpha CPU 的编译是否与 UB 无关。就像有符号整数溢出一样,即使在为具有非常明确的语义的 2 的补码机器编译时也是如此。) -
“不破坏单线程代码”规则仅意味着编写它的线程无需检查即可读取它,但每个 其他 线程都可以看到重新排序。即单个线程按顺序看到自己的操作,但其他线程看到它们发生乱序。
-
“其中任何一个都将允许指针在 std::string 类中的数据仍在写入时读取为非 NULL” - 这不与加载指针的语句相矛盾吗?然后取消引用它可以保证你得到的数据至少和指针一样新?
标签: multithreading c++11 atomic lock-free memory-barriers