【问题标题】:lock-free synchronization, fences and memory order (store operation with acquire semantics)无锁同步、栅栏和内存顺序(具有获取语义的存储操作)
【发布时间】:2017-01-01 21:39:39
【问题描述】:

我正在将一个在准系统上运行的项目迁移到 linux,并且需要消除一些 {disable,enable}_scheduler 调用。 :)

所以我需要在单个写入器、多个读取器的场景中使用无锁同步解决方案,其中写入器线程不能被阻塞。我想出了以下解决方案,它不适合通常的获取-释放顺序:

class RWSync {
    std::atomic<int> version; // incremented after every modification
    std::atomic_bool invalid; // true during write
public:
  RWSync() : version(0), invalid(0) {}
  template<typename F> void sync(F lambda) {
    int currentVersion;
    do {
      do { // wait until the object is valid
        currentVersion = version.load(std::memory_order_acquire);
      } while (invalid.load(std::memory_order_acquire));
      lambda();
      std::atomic_thread_fence(std::memory_order_seq_cst);
      // check if something changed
    } while (version.load(std::memory_order_acquire) != currentVersion
        || invalid.load(std::memory_order_acquire));
  }
  void beginWrite() {
    invalid.store(true, std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_seq_cst);
  }
  void endWrite() {
    std::atomic_thread_fence(std::memory_order_seq_cst);
    version.fetch_add(1, std::memory_order_release);
    invalid.store(false, std::memory_order_release);
  }
}

我希望意图很明确:我在beginWrite/endWrite 之间包装了一个(非原子)有效负载的修改,并仅在传递给sync() 的 lambda 函数内读取有效负载。

如您所见,我在beginWrite() 中有一个原子存储,在存储操作之后没有写入可以在存储之前重新排序。我没有找到合适的例子,而且我完全没有这方面的经验,所以我想确认一下它是可以的(通过测试验证也不容易)。

  1. 此代码是否无竞争且按预期工作?

  2. 如果我在每个原子操作中都使用 std::memory_order_seq_cst,我可以省略栅栏吗? (即使是,我估计性能会更差)

  3. 我可以在 endWrite() 中放下围栏吗?

  4. 我可以在栅栏中使用 memory_order_acq_rel 吗?我真的不明白其中的区别——我不清楚单个总订单的概念。

  5. 是否有任何简化/优化的机会?

+1。我很乐意接受任何更好的想法作为这个类的名称:)

【问题讨论】:

  • 作为一个建议:对我来说,使用 Relacy Race Detector 大大简化了处理此类问题的过程。 SPIN/Promela 等经典工具不直接支持高级内存模型。 github.com/dvyukov/relacy
  • 您的代码已损坏:如果编写器碰巧在阅读器执行version.load(std::memory_order_acquire) != currentVersion)invalid.load(std::memory_order_acquire) 条件之间执行endWrite(),则阅读器不会看到版本凸起或引发的无效标志,即使它的代码与编写器代码并发运行。在这种情况下,作者可能早在读者完成对无效标志的等待循环时就调用了beginWrite(),显然破坏了读取数据的一致性。
  • 你是对的。如果我在 do..while() 条件下更改测试顺序,先检查无效,再检查版本怎么办?

标签: c++11 atomic lock-free memory-fences memory-barriers


【解决方案1】:

代码基本正确。

除了有两个原子变量(versioninvalid),您可以使用 single version 变量与语义“奇数值无效”。这就是所谓的“顺序锁”机制。

减少原子变量的数量大大简化了事情:

class RWSync {
    // Incremented before and after every modification.
    // Odd values mean that object in invalid state.
    std::atomic<int> version; 
public:
  RWSync() : version(0) {}
  template<typename F> void sync(F lambda) {
    int currentVersion;
    do {
      currentVersion = version.load(std::memory_order_seq_cst);
      // This may reduce calls to lambda(), nothing more
      if(currentVersion | 1) continue;

      lambda();

      // Repeat until something changed or object is in an invalid state.
    } while ((currentVersion | 1) ||
        version.load(std::memory_order_seq_cst) != currentVersion));
  }
  void beginWrite() {
    // Writer may read version with relaxed memory order
    currentVersion = version.load(std::memory_order_relaxed);
    // Invalidation requires sequential order
    version.store(currentVersion + 1, std::memory_order_seq_cst);
  }
  void endWrite() {
    // Writer may read version with relaxed memory order
    currentVersion = version.load(std::memory_order_relaxed);
    // Release order is sufficient for mark an object as valid
    version.store(currentVersion + 1, std::memory_order_release);
  }
};

注意beginWrite()endWrite()中内存顺序的区别:

  • endWrite() 确保所有先前对象的修改都已完成。使用 release 内存顺序就足够了。

  • beginWrite() 确保阅读器会在任何进一步对象的修改开始之前检测到对象处于无效状态。这样的保证需要 seq_cst 内存顺序。因为那个阅读器也使用 seq_cst 内存顺序。

至于栅栏,最好将它们合并到先前/未来的原子操作中:编译器知道如何快速生成结果。


原代码部分修改说明:

1) 像fetch_add() 这样的原子修改 适用于并发修改(像另一个fetch_add())可能的情况。为了正确起见,此类修改使用内存锁定或其他非常耗时架构特定的东西。

原子赋值 (store()) 不使用内存锁定,因此它比fetch_add()便宜。您可以使用这样的分配,因为在您的情况下不可能进行并发修改(读者不会修改version)。

2) 与区分loadstore 操作的release-acquire 语义不同,顺序一致性(memory_order_seq_cst)适用于每个原子访问,并提供这些操作之间的总顺序访问。

【讨论】:

  • 不,OP给出的代码不正确。请参阅我对这个问题的评论。
  • 您的代码是正确的,因为它消除了检查版本和无效标志之间的竞争。
  • 感谢您的正确实施!一些问题:
  • 1) 为什么使用单独的加载/存储而不是 fetch_add()? 2) 我使用了栅栏,因为我解释了 store() 永远不能成为获取操作的文档,因为这仅适用于加载操作。我想这就是 seq_cst 出现的情况;所以在这种情况下,即使 acq_rel 在 beginWrite() 和 sync() 中也不够用?
  • 1) 查看答案的编辑。 2)内存顺序seq_cst也可用于store(),请参阅答案的编辑。 acq_rel 语义不适用于 beginWrite(),因为它只能将事件 previous 排序到 store()。至于 fences,在您的情况下,它们仅添加具有相同内存顺序的原子操作。
【解决方案2】:

接受的答案不正确。我猜代码应该类似于“currentVersion & 1”而不是“currentVersion | 1”。更微妙的错误是,读取线程可以进入 lambda(),然后写入线程可以运行 beginWrite() 并将值写入非原子变量。在这种情况下,有效负载中的写入操作和有效负载中的读取操作没有发生之前的关系。对非原子变量的并发访问(没有发生之前的关系)是一种数据竞争。注意,memory_order_seq_cst 的单个总顺序并不意味着happens-before关系;它们是一致的,但有两种情况。

【讨论】:

    猜你喜欢
    • 2016-08-17
    • 2017-02-28
    • 1970-01-01
    • 2011-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-08
    • 1970-01-01
    相关资源
    最近更新 更多