1

我正在将一个在准系统上运行的项目迁移到 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().

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

  1. 这段代码是无竞争的并且可以按我的预期工作吗?

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

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

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

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

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

4

2 回答 2

1

代码基本正确。

您可以使用具有语义“奇数值无效”的单个变量,而不是使用两个原子变量 (version和)。这被称为“顺序锁定”机制。invalid 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()确保所有先前对象的修改都已完成。为此使用释放内存顺序就足够了。

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

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


原代码部分修改说明:

1)类似的原子修改fetch_add()适用于可以同时修改(如另一个fetch_add())的情况。为了正确起见,此类修改使用内存锁定或其他非常耗时的架构特定事物。

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

2) 与区分和操作的释放-获取语义不同,顺序一致性()适用于每个原子访问,并提供这些访问之间的总顺序。loadstorememory_order_seq_cst

于 2017-01-11T09:30:48.523 回答
0

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

于 2017-06-10T06:52:18.253 回答