当某些错误可以接受时,顺序加载存储原子的内存顺序应该是啥
Posted
技术标签:
【中文标题】当某些错误可以接受时,顺序加载存储原子的内存顺序应该是啥【英文标题】:What should be memory order for sequential load-store atomics when certain errors are acceptable当某些错误可以接受时,顺序加载存储原子的内存顺序应该是什么 【发布时间】:2017-01-30 01:56:37 【问题描述】:假设用户正在转动 MIDI 控制器上的旋钮,并且这些值作为存储值的增量和减量发送到我的程序。单向转动旋钮将发送一系列减量,其值取决于旋转速度;扭转另一种方式增量。我想将存储的值(以及以下函数发出的值)保持在 0 到 100 之间。如果删除一条或几条消息,那没什么大不了的,但我不希望在OffsetResult_
函数发出的值。
那么我的问题是——以下内存顺序指令看起来正确吗?对我来说最清楚的是compare_exchange_strong
。该程序将其用作可能失败的store
,因此似乎释放内存排序适用。
我什至可以去std::memory_order_relaxed
,因为我主要关心的是对storedV 的更改的原子性,而不是记住对storedV 的每次更改?
有没有一种通用的方法来查看组合的加载/存储函数,以确定它是否应该是获取、释放或顺序一致的?
class ChannelModel
ChannelModel():currentV0;
int OffsetResult_(int diff) noexcept;
private:
atomic<int> storedV;
;
int ChannelModel::OffsetResult_(int diff) noexcept
int currentV = storedV.fetch_add(diff, std::memory_order_acquire) + diff;
if (currentV < 0) //fix storedV unless another thread has already altered it
storedV.compare_exchange_strong(currentV, 0, std::memory_order_release, std::memory_order_relaxed);
return 0;
if (currentV > 100) //fix storedV unless another thread has already altered it
storedV.compare_exchange_strong(currentV, 100, std::memory_order_release, std::memory_order_relaxed);
return 100;
return currentV;
请注意,实际代码要复杂得多,有理由相信来自控制器的每条消息的响应将花费足够长的时间,以至于有时会由两个线程几乎同时调用此函数.
【问题讨论】:
1.fetch_add(...) + diff
添加了两次diff
,不是吗? 2. fetch_add
会不会导致 storedV = storedV + diff
可能是 < 0
或 > 100
进而可能对其他线程可见?
fetch_add 返回原始值并存储加法的结果。因此,为了能够在函数中正确使用结果,而无需再次获取原子(并且可能存在并发问题),我还将 diff 添加到获取的值中。
是的,越界值可能对其他线程可见,但这不是主要问题,因为函数永远不会返回越界值。所以用户不会注意到问题,除了开始向相反方向移动控制器时可能会有非常轻微的延迟。我的目标是让每个线程都有一个内部一致的值视图——这就是为什么大部分工作都是使用临时变量完成的。
这是一个略有不同的问题域,但您可能感兴趣的是CRDTs
【参考方案1】:
我会假设currentV
是OffsetResult_
中的一个局部变量。
由于某种原因,它在类构造函数中被初始化,但没有定义为类变量。
您正在使用fetch_add
更改storedV
的值,然后使用compare_exchange_strong
调整可能出现的错误。
这是不正确的...compare_exchange_strong
在这里用作条件store
。只有当另一个线程没有更改该值时,storedV
才会被更新。
您指定的内存排序不正确.. 通常,release
排序与原子 store
一起使用,表示数据已“释放”,
IE。可用于另一个线程,该线程将使用acquire
排序从同一原子load
。 release
和 acquire
排序形成运行时关系并且总是成对出现。
当currentV
在其定义的范围内时,您的代码中缺少这种关系,因为您从未执行过release
操作。
不清楚为什么要指定排序。请注意,您不必设置内存顺序,在这种情况下,将使用(更安全的)默认值 (std::memory_order_seq_cst
)。
较弱的排序是否正确取决于它在线程之间同步的数据。
在没有数据依赖的情况下,使用 std::memory_order_relaxed
可能是正确的,但代码中缺少该上下文。
但是,由于原子与旋钮值相关联,因此旋转旋钮可能会导致一些涉及其他数据的操作。
我不会尝试在这里使用较弱的内存排序进行优化。可能根本没有任何好处,因为 Read-Modify-Write 调用 (compare_exchange_x
) 已经
相对昂贵。此外,如果使用较弱的内存排序会引入错误,则将很难非常进行调试。
您可以使用std::compare_exchange_weak
进行调整而不会丢失更新:
int ChannelModel::OffsetResult_(int diff) noexcept
int updatedV;
int currentV = storedV.load();
do
updatedV = currentV + diff;
if (updatedV > 100)
updatedV = 100;
else if (updatedV < 0)
updatedV = 0;
while (!storedV.compare_exchange_weak(currentV, updatedV));
return updatedV;
关键是compare_exchange_weak
只会(原子地)更新storedV
,如果它仍然(或再次)等于currentV
。
如果该检查失败,它将再次遍历循环。
在循环中使用,compare_exchange_weak
(可能会虚假失败)是比compare_exchange_strong
更好的选择。
内存排序是一个复杂的话题,here 是一个很好的概述。
【讨论】:
我不担心丢失更新,因此无需为 compare_exchange 循环。关于内存排序的有趣评论。使用英特尔处理器,对内存排序大惊小怪有什么真正的好处吗? 修复我的最后一条评论——我在询问 compare_exchange 的内存排序。听起来它足够昂贵,以至于改变内存顺序是不值得的。对吗? @rsjaffe 完全正确。RMW 很昂贵,因为它必须锁定总线才能与访问数据的其他内核同步。在您的代码中,对于fetch_add
和compare_exchange_strong
,您将分别看到像lock xadd
和lock cmpxchg
这样的目标代码。在 X86 上,你使用的和seq_cst
排序没有区别以上是关于当某些错误可以接受时,顺序加载存储原子的内存顺序应该是啥的主要内容,如果未能解决你的问题,请参考以下文章
C++ 原子内存顺序与诸如 notify() 之类的线程事件
详解java中CAS机制所导致的问题以及解决——内存顺序冲突