当某些错误可以接受时,顺序加载存储原子的内存顺序应该是啥

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 可能是 &lt; 0&gt; 100 进而可能对其他线程可见? fetch_add 返回原始值并存储加法的结果。因此,为了能够在函数中正确使用结果,而无需再次获取原子(并且可能存在并发问题),我还将 diff 添加到获取的值中。 是的,越界值可能对其他线程可见,但这不是主要问题,因为函数永远不会返回越界值。所以用户不会注意到问题,除了开始向相反方向移动控制器时可能会有非常轻微的延迟。我的目标是让每个线程都有一个内部一致的值视图——这就是为什么大部分工作都是使用临时变量完成的。 这是一个略有不同的问题域,但您可能感兴趣的是CRDTs 【参考方案1】:

我会假设currentVOffsetResult_ 中的一个局部变量。 由于某种原因,它在类构造函数中被初始化,但没有定义为类变量。

您正在使用fetch_add 更改storedV 的值,然后使用compare_exchange_strong 调整可能出现的错误。 这是不正确的...compare_exchange_strong 在这里用作条件store。只有当另一个线程没有更改该值时,storedV 才会被更新。 您指定的内存排序不正确.. 通常,release 排序与原子 store 一起使用,表示数据已“释放”, IE。可用于另一个线程,该线程将使用acquire 排序从同一原子loadreleaseacquire 排序形成运行时关系并且总是成对出现。 当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_addcompare_exchange_strong,您将分别看到像lock xaddlock cmpxchg 这样的目标代码。在 X86 上,你使用的和seq_cst 排序没有区别

以上是关于当某些错误可以接受时,顺序加载存储原子的内存顺序应该是啥的主要内容,如果未能解决你的问题,请参考以下文章

C++ 原子内存顺序与诸如 notify() 之类的线程事件

深入理解Atomic原子操作和volatile非原子性

详解java中CAS机制所导致的问题以及解决——内存顺序冲突

代码不能接受超过数组大小的文件?

Java内存模型原子性内存可见性重排序顺序一致性volatile锁final

类加载机制