为什么在 CAS 循环失败时首选 std::memory_order_relaxed?
Why std::memory_order_relaxed is preferred at CAS loop when failing?
在实施 CAS Loop using std::atomic
, cppreference in this link 时给出了 push
的以下示例:
template<typename T>
class stack
{
std::atomic<node<T>*> head;
public:
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
new_node->next = head.load(std::memory_order_relaxed);
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed /* Eh? */));
}
};
现在,我不明白为什么 std::memory_order_relaxed
用于失败案例,因为据我所知,compare_exchange_weak
(与 -strong 但为了方便我只使用弱版本)是失败时的加载操作,这意味着它从另一个线程中成功的 CAS 操作加载 std::memory_order_release
,因此它应该使用 std::memory_order_acquire
改为 与 同步......?
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_acquire /* There you go! */));
假设,如果 'relaxed load' 获得旧值之一,最终一次又一次地失败,在循环中停留额外的时间怎么办?
下图是我脑洞大开的地方
Shouldn't a store from T2 be visible at T1? (by having
synchronized-with relation with each other)
总结一下我的问题,
- 为什么在失败时不
std::memory_order_acquire
而不是 std::memory_order_relaxed
?
- 是什么让
std::memory_order_relaxed
足够了?
- 失败时
std::memory_order_relaxed
是否意味着 (可能) 更多循环?
- 同样,失败时
std::memory_order_acquire
是否意味着 (可能) 更少循环? (除了性能的缺点)
I don't understand how come std::memory_order_relaxed is used for the
failure
而且我不明白你怎么抱怨那个失败分支上缺少获取语义却不抱怨
head.load(std::memory_order_relaxed);
然后
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release
两者都没有获取操作 "to be synchronized-with" 您没有向我们展示的一些其他操作。您关心的其他操作是什么?
如果该操作很重要,请显示该操作并告诉用户该代码如何依赖于该其他操作的 "publication"(或 "I'm done" 信号)。
答案:push
函数绝不依赖于其他函数发布的任何"I'm done"信号,因为推送不使用其他发布的信号数据,不读取其他推送的元素等
Why not std::memory_order_acquire, instead of
std::memory_order_relaxed at failure?
获得什么?也就是说,要观察什么修养?
Does std::memory_order_relaxed at failure mean (potentially) more
looping?
没有。故障模式与内存可见性无关;这是 CPU 缓存机制的一个功能。
编辑:
我刚看到你图片中的文字:
Shouldn't a store from T2 be visible at T1? (by having
synchronized-with relation with each other)
实际上您误解了 synchronized-with:它不会传播正在读取的原子变量的值,因为原子根据定义是可用于竞争条件的原语。 原子的读取总是returns原子变量的值,由其他线程(或同一线程)写入。如果不是这样,那么任何原子操作都没有意义。
读取单个原子变量不需要内存排序。
更严格的内存顺序用于防止数据竞争,不应提高已经正确的程序的性能。
在您提供的示例中,将 memory_order_relaxed
替换为 memory_order_acquire
不会解决任何数据竞争,只会降低性能。
为什么没有数据竞争?因为 while 循环仅适用于单个原子,无论使用的内存顺序如何,它始终是 data-race-free。
为什么在成功的情况下使用memory_order_release
?示例中没有显示,但假设head的访问使用memory_order_acquire
,例如:
T* stack::top() {
auto h = head.load(std::memory_order_acquire);
return h ? &h->value() : nullptr;
}
这个release-acquire序列在释放新头和通过另一个线程。
Thread A
Thread B
st.push(42);
if (auto value = st.top()) {
assert(*value == 42);
}
在上面的示例中,如果没有 release-acquire(如果使用 memory_order_relaxed
),断言可能会失败,因为 Thread B 可以看到 head
已经指向的未完全初始化的 node
(编译器甚至可以在 push()
中设置 head
下面重新排序 node
构造函数调用)。换句话说会有数据竞争。
在实施 CAS Loop using std::atomic
, cppreference in this link 时给出了 push
的以下示例:
template<typename T>
class stack
{
std::atomic<node<T>*> head;
public:
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
new_node->next = head.load(std::memory_order_relaxed);
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed /* Eh? */));
}
};
现在,我不明白为什么 std::memory_order_relaxed
用于失败案例,因为据我所知,compare_exchange_weak
(与 -strong 但为了方便我只使用弱版本)是失败时的加载操作,这意味着它从另一个线程中成功的 CAS 操作加载 std::memory_order_release
,因此它应该使用 std::memory_order_acquire
改为 与 同步......?
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_acquire /* There you go! */));
假设,如果 'relaxed load' 获得旧值之一,最终一次又一次地失败,在循环中停留额外的时间怎么办?
下图是我脑洞大开的地方
Shouldn't a store from T2 be visible at T1? (by having synchronized-with relation with each other)
总结一下我的问题,
- 为什么在失败时不
std::memory_order_acquire
而不是std::memory_order_relaxed
? - 是什么让
std::memory_order_relaxed
足够了? - 失败时
std::memory_order_relaxed
是否意味着 (可能) 更多循环? - 同样,失败时
std::memory_order_acquire
是否意味着 (可能) 更少循环? (除了性能的缺点)
I don't understand how come std::memory_order_relaxed is used for the failure
而且我不明白你怎么抱怨那个失败分支上缺少获取语义却不抱怨
head.load(std::memory_order_relaxed);
然后
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release
两者都没有获取操作 "to be synchronized-with" 您没有向我们展示的一些其他操作。您关心的其他操作是什么?
如果该操作很重要,请显示该操作并告诉用户该代码如何依赖于该其他操作的 "publication"(或 "I'm done" 信号)。
答案:push
函数绝不依赖于其他函数发布的任何"I'm done"信号,因为推送不使用其他发布的信号数据,不读取其他推送的元素等
Why not std::memory_order_acquire, instead of std::memory_order_relaxed at failure?
获得什么?也就是说,要观察什么修养?
Does std::memory_order_relaxed at failure mean (potentially) more looping?
没有。故障模式与内存可见性无关;这是 CPU 缓存机制的一个功能。
编辑:
我刚看到你图片中的文字:
Shouldn't a store from T2 be visible at T1? (by having synchronized-with relation with each other)
实际上您误解了 synchronized-with:它不会传播正在读取的原子变量的值,因为原子根据定义是可用于竞争条件的原语。 原子的读取总是returns原子变量的值,由其他线程(或同一线程)写入。如果不是这样,那么任何原子操作都没有意义。
读取单个原子变量不需要内存排序。
更严格的内存顺序用于防止数据竞争,不应提高已经正确的程序的性能。
在您提供的示例中,将 memory_order_relaxed
替换为 memory_order_acquire
不会解决任何数据竞争,只会降低性能。
为什么没有数据竞争?因为 while 循环仅适用于单个原子,无论使用的内存顺序如何,它始终是 data-race-free。
为什么在成功的情况下使用memory_order_release
?示例中没有显示,但假设head的访问使用memory_order_acquire
,例如:
T* stack::top() {
auto h = head.load(std::memory_order_acquire);
return h ? &h->value() : nullptr;
}
这个release-acquire序列在释放新头和通过另一个线程。
Thread A | Thread B |
---|---|
st.push(42); |
if (auto value = st.top()) { |
在上面的示例中,如果没有 release-acquire(如果使用 memory_order_relaxed
),断言可能会失败,因为 Thread B 可以看到 head
已经指向的未完全初始化的 node
(编译器甚至可以在 push()
中设置 head
下面重新排序 node
构造函数调用)。换句话说会有数据竞争。