1

让我们考虑以下使用链表实现的单读/单写队列。

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;
}

现在我发现 usingvolatile int n可以让我的 writer 运行得更快,而我不确定它是否能保证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读者永远不会访问哪个?这会保证 和 的顺序tail->next = ithead = head->next?(不过,它比使用 atomic 运行得更快n

4

2 回答 2

2

C++ 中的volatile关键字不是为变量读/写保证在多线程环境中在代码中看起来如此有序的构造。因此,在您的代码中,原子模板包装的计数器仅使用关键字裸露,volatile消费者线程观察到的计数器的增加并不能保证item::next也已更新。

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

于 2020-02-19T05:25:10.800 回答
0

正如在其他几条评论中已经指出的那样, volatile 与多线程无关,因此不应此处使用。但是,volatile 比 atmoic 性能更好的原因仅仅是因为 volatile++n转换为简单的加载、inc、存储指令,而 atomic 转换为更昂贵的lock xadd(假设您为 x86 编译)。

但是由于这只是一个单读单写队列,所以不需要昂贵的读-修改-写操作:

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;
}

这应该与 volatile 版本一样好。如果acquire-load in pop“看到”了store-release in 写入的值push,则这两个操作相互同步,从而建立所需的happens-before关系。

于 2020-02-19T10:53:12.543 回答