无锁同步,围栏和内存顺序(具有获取语义的存储操作)

时间:2017-01-01 21:39:39

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

我正在将一个在裸机上运行的项目迁移到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. 是否有任何简化/优化机会?

  6. 1。我高兴地接受任何更好的主意作为这个类的名称:)

2 个答案:

答案 0 :(得分:1)

代码基本上是正确的。

您可以使用语义为“奇数值无效”的单个 version变量,而不是使用两个原子变量(invalidversion)。这被称为“顺序锁定”机制。

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

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

答案 1 :(得分:0)

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