22

考虑以下取自 Herb Sutter 关于原子的演讲的代码片段:

smart_ptr 类包含一个名为 control_block_ptr 的 pimpl 对象,其中包含引用计数refs

// Thread A:
// smart_ptr copy ctor
smart_ptr(const smart_ptr& other) {
  ...
  control_block_ptr = other->control_block_ptr;
  control_block_ptr->refs.fetch_add(1, memory_order_relaxed);
  ...
}

// Thread D:
// smart_ptr destructor
~smart_ptr() {
  if (control_block_ptr->refs.fetch_sub(1, memory_order_acq_rel) == 1) {
    delete control_block_ptr;
  }
}

Herb Sutter 说线程 A 中refs的增量可以使用 memory_order_relaxed 因为“没有人根据动作做任何事情”。现在,据我了解 memory_order_relaxed,如果refs在某个时候等于 N,并且两个线程 A 和 B 执行以下代码:

control_block_ptr->refs.fetch_add(1, memory_order_relaxed);

那么可能会发生两个线程都看到refs的值为N 并且都将 N+1 写回它的情况。这显然行不通,应该像使用析构函数一样使用 memory_order_acq_rel。我哪里错了?

EDIT1:考虑以下代码。

atomic_int refs = N; // at time t0. 

// [Thread 1]
refs.fetch_add(1, memory_order_relaxed); // at time t1. 

// [Thread 2]
n = refs.load(memory_order_relaxed);   // starting at time t2 > t1
refs.fetch_add(1, memory_order_relaxed);
n = refs.load(memory_order_relaxed);

在调用 fetch_add 之前,线程 2 观察到的refs的值是多少?可以是 N 还是 N+1?调用 fetch_add 后线程 2 观察到的 refs 的值是多少?必须至少为 N+2 吗?

[谈话网址:C++ & Beyond 2012 - http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-2-of-2 (@ 1: 20:00)]

4

3 回答 3

10

Boost.Atomic 模拟库std::atomic提供了类似的引用计数示例和解释,它可能有助于您的理解。

始终可以通过以下方式增加引用计数器memory_order_relaxed:对对象的新引用只能由现有引用形成,并且将现有引用从一个线程传递到另一个线程必须已经提供任何所需的同步。

在删除另一个线程中的对象之前,强制在一个线程中(通过现有引用)对对象进行任何可能的访问,这一点很重要。这是通过删除引用后的“释放”操作(显然之前必须通过此引用访问对象)和删除对象之前的“获取”操作来实现的。

可以使用memory_order_acq_relfetch_sub 操作,但是当引用计数器尚未达到零时,这会导致不需要的“获取”操作,并且可能会造成性能损失。

于 2014-12-31T05:05:49.683 回答
4

来自 C++ 参考std::memory_order

memory_order_relaxed:宽松操作:对其他读写没有同步或排序约束,只有这个操作的原子性得到保证

该页面下面还有一个示例。

所以基本上,std::atomic::fetch_add()即使使用 ,仍然是原子的std::memory_order_relaxed,因此来自 2 个不同线程的并发refs.fetch_add(1, std::memory_order_relaxed)将始终递增2。内存顺序的要点是如何在指定内存顺序的当前原子操作周围重新排序refs其他非原子或原子操作std::memory_order_relaxed.

于 2017-07-02T04:36:47.213 回答
2

由于这相当令人困惑(至少对我而言),我将部分解决一个问题:

(...)那么可能会发生两个线程都看到 refs 的值是 N 并且都将 N+1 写回它(...)

根据@AnthonyWilliams 在这个答案中的说法,上面的句子似乎是错误的:

保证您拥有“最新”值的唯一方法是使用读-修改-写操作,例如 exchange()、compare_exchange_strong() 或 fetch_add()。读-修改-写操作有一个额外的约束,即它们总是对“最新”值进行操作,因此一系列线程的 ai.fetch_add(1) 操作序列将返回一个没有重复或间隙的值序列。在没有其他约束的情况下,仍然无法保证哪些线程会看到哪些值。

因此,鉴于权威论点,我想说两个线程都不可能看到从NN+1的值。

于 2016-12-08T17:25:55.217 回答