为什么不赞成使用std :: shared_ptr :: unique()?

时间:2016-12-14 12:10:42

标签: c++ multithreading shared-ptr c++17

std::shared_ptr::unique()的技术问题是在C ++ 17中弃用的原因是什么?

根据cppreference.com,{+ 1}}在C ++ 17中被弃用为

  

从C ++ 17开始,不推荐使用此函数,因为std::shared_ptr::unique()只是多线程环境中的近似值。

我理解这对于use_count是正确的:虽然我持有对它的引用,但其他人可能会同时放弃他或创建新副本。

<击>

但是如果use_count() > 1返回1(这是我在调用use_count()时感兴趣的话)那么就没有其他线程可以以一种活泼的方式改变这个值,所以我会期望这应该是安全的:

unique()

<击>

我自己搜索的结果:

我找到了这个文档:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0521r0.html建议弃用以回应 C ++ 17 CD评论CA 14 ,但我自己找不到评论。

作为替代方案,该文件建议增加一些注释,包括以下内容:

  

注意:当多个线程影响if (myPtr && myPtr.unique()) { //Modify *myPtr } 的返回值时,结果应视为近似值。 特别是,use_count()并不意味着通过以前被破坏的use_count() == 1的访问在任何意义上都已完成。 - 结束注释

我理解这可能是shared_ptr当前指定的方式(由于缺乏保证的同步),但为什么解决方案不仅仅是指定这样的同步,因此使上述模式安全?如果存在不允许这种同步的基本限制(或使其成本高昂),那么如何正确实现析构函数呢?

更新

我忽略了@ alexeykuzmin0和@rubenvb提出的明显案例,因为到目前为止我只在use_count()的实例上使用了unique()其他线程本身无法访问的实例。因此,没有任何危险,特定的实例将以一种活泼的方式被复制。

我仍然有兴趣了解CA 14究竟是什么,因为我相信shared_ptr的所有用例只要能保证与不同的{{1}发生的事情同步就会起作用其他线程上的实例。所以它对我来说似乎仍然是一个有用的工具,但我可能会忽略一些基本的东西。

为了说明我的想法,请考虑以下事项:

unique()

它是否有任何问题(如果shared_ptr实际上与其他副本的析构函数同步)?

4 个答案:

答案 0 :(得分:12)

请考虑以下代码:

// global variable
std::shared_ptr<int> s = std::make_shared<int>();

// thread 1
if (s && s.unique()) {
    // modify *s
}

// thread 2
auto s2 = s;

这里我们有一个经典的竞争条件:s2可能(或可能不)在线程2中创建s的副本,而线程1在if内。

unique() == true表示没有人shared_ptr指向同一个内存,但并不意味着任何其他线程无法直接或通过指针或引用访问初始shared_ptr

答案 1 :(得分:7)

我认为P0521R0通过滥用shared_ptr作为线程间同步来解决潜在的数据竞争。 它说use_count()返回不可靠的引用计数值,因此,unique()成员函数在多线程时将无用。

int main() {
  int result = 0;
  auto sp1 = std::make_shared<int>(0);  // refcount: 1

  // Start another thread
  std::thread another_thread([&result, sp2 = sp1]{  // refcount: 1 -> 2
    result = 42;  // [W] store to result
    // [D] expire sp2 scope, and refcount: 2 -> 1
  });

  // Do multithreading stuff:
  //   Other threads may concurrently increment/decrement refcounf.

  if (sp1.unique()) {      // [U] refcount == 1?
    assert(result == 42);  // [R] read from result
    // This [R] read action cause data race w.r.t [W] write action.
  }

  another_thread.join();
  // Side note: thread termination and join() member function
  // have happens-before relationship, so [W] happens-before [R]
  // and there is no data race on following read action.
  assert(result == 42);
}

成员函数unique()没有任何同步效果,并且从[D] shared_ptr的析构函数到[U]调用之前没有发生在之前的关系unique()。 所以我们不能指望关系[W]⇒[D]⇒[U]⇒[R]和[W]⇒[R]。 ('⇒'表示发生在关系之前)。

已编辑:我发现了两个相关的LWG问题; LWG2434. shared_ptr::use_count() is efficientLWG2776. shared_ptr unique() and use_count()。这只是一个推测,但WG21委员会优先考虑C ++标准库的现有实现,因此他们在C ++ 1z中编写了它的行为。

LWG2434引用(强调我的):

  

shared_ptrweak_ptr注意到他们的use_count()可能效率低下。 这是尝试确认重新链接的实现(例如,可以由Loki智能指针使用)。 但是,没有任何shared_ptr实现使用重新链接,尤其是在C ++ 11认识到存在多线程之后。每个人都使用原子引用,因此 use_count()只是一个原子加载

LWG2776引用(强调我的):

  

LWG 2434删除了use_count()unique() shared_ptr中的“仅调试”限制,引入了一个错误。为了使unique()生成有用且可靠的值,它需要一个synchronize子句,以确保通过另一个引用的先前访问对unique()的成功调用者可见。 许多当前的实现使用宽松的负载,并且不提供此保证,因为标准中没有说明。对于调试/提示使用,这是正常的。没有它,规范不清楚,可能会产生误导。

     

[...]

     

我更愿意将use_count()指定为仅提供实际计数的不可靠提示(另一种说明仅调试的方式)。或者像JF建议的那样弃用它。 我们无法在不增加更多击剑的情况下使use_count()可靠。我们真的不希望有人等待use_count() == 2确定另一个线程到那么远。不幸的是,我认为我们目前没有说什么来表明这是一个错误。

     

这意味着use_count()通常使用memory_order_relaxed,并且use_count()并未指定或实施唯一。

答案 2 :(得分:3)

为了您的观赏乐趣:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0488r0.pdf

本文件包含所有NB(国家机构)对Issaquah会议的评论。 CA 14读到:

  

删除use_count()和的“仅调试”限制   shared_ptr中的unique()引入了一个bug:为了unique()来   产生一个有用且可靠的值,它需要一个synchronize子句   确保通过其他参考的先前访问可见   unique()的成功调用者。许多当前的实现使用a   放松的负荷,并没有提供这种保证,因为它没有说明   在标准中。对于调试/提示用法,这是正常的。没有它了   规范不明确且具有误导性。

答案 3 :(得分:-1)

std::enable_shared_from_this 的存在是制造任何问题的麻烦制造者 unique() 的有趣用法。实际上,std::enable_shared_from_this 允许从任何线程的原始指针创建新的 shared_ptr。这意味着 unique() 永远不能保证任何事情。

但考虑另一个库...虽然这与 shared_ptr 无关,但在 Qt 中,有一个名为 isDetached()内部方法,其实现(几乎)与unique()。 它用于一些非常有用的优化目的:当 true 时,可以改变指向的对象而无需执行“写时复制”操作。 实际上,一旦唯一,托管资源就不能被源自另一个线程的操作共享。如果 enable_shared_from_this 不存在,shared_ptr 也可以使用相同的模式。

这就是恕我直言,unique() 已从 C++20 中删除的原因:误导。