我可以在 one-reader/one-writer 队列中用 volatile 替换 atomic 吗?

Can I replace the atomic with a volatile in an one-reader/one-writer queue?

让我们考虑以下使用链表实现的one-reader/one-writer队列。

struct queue {
  queue() {
    tail = head = &reserved;
    n = 0;
  }
  void push(item *it) {
     tail->next = it;
     tail = it;
     n++;
  }
  item* pop() {
     while (n == used);
     ++used;
     head = head->next;
     return head;
  }

  item reserved;
  item *tail, *head;

  int used = 0;
  std::atomic <int> n;
}

现在我发现使用 volatile int n 可以使我的编写器 运行 更快,但我不确定它是否保证 head = head->next 始终可以读取正确的值。

更新:如果在 tail->nextn++ 之间添加一个原子操作,即

会怎么样
void push(item *it) {
   tail->next = it;
   tail = it;
   a.store(0, std::memory_order_release);
   a.load(std::memory_order_acquire);
   n++;
}

其中 a 从未被 reader 访问过?这样能保证tail->next = ithead = head->next的顺序吗? (不过,它 运行s 比使用 atomic n 快)

C++ 中的 volatile 关键字不是为变量 read/write 提供保证在多线程环境中按照代码中的顺序排列的结构。因此,在您的代码中,原子模板包装计数器仅使用 volatile 关键字就可以了,增加消费者线程观察到的计数器并不能保证 item::next 也已更新。

为了在保证的情况下实现最大性能,我认为至少你必须在更新 head->next 和计数器的增量之间插入一个写屏障,例如通过 n.fetch_add(1, std::memory_order_release),以及在获取 tail->next 之前的读取屏障,例如 n.load(std::memory_order_acquire)。不过,我不知道 CPU-arch 的具体细节。

正如其他几条评论中已经指出的那样,volatile 与多线程无关,因此这里应该使用它。然而,volatile 比 atmoic 表现更好的原因很简单,因为 volatile ++n 转换为简单的加载、inc、存储指令,而 atomic 则转换为更昂贵的 lock xadd(假设你为 x86 编译)。

但由于这只是一个 reader 单写入器队列,因此您不需要昂贵的读取-修改-写入操作:

struct queue {
  queue() {
    tail = head = &reserved;
    n = 0;
  }
  void push(item *it) {
     tail->next = it;
     tail = it;
     auto new_n = n.load(std::memory_order_relaxed) + 1;
     n.store(new_n, std::memory_order_release);
  }
  item* pop() {
     while (n.load(std::memory_order_acquire) == used);
     ++used;
     head = head->next;
     return head;
  }

  item reserved;
  item *tail, *head;

  int used = 0;
  std::atomic <int> n;
}

这应该与易变版本一样好。如果acquire-load在pop "sees" store-release写入的值在push,两个操作同步,从而建立所需的happens-before关系。