为啥不推荐使用 std::shared_ptr::unique() ?

Posted

技术标签:

【中文标题】为啥不推荐使用 std::shared_ptr::unique() ?【英文标题】:Why is std::shared_ptr::unique() deprecated?为什么不推荐使用 std::shared_ptr::unique() ? 【发布时间】:2016-12-14 12:10:41 【问题描述】:

std::shared_ptr::unique() 的技术问题是什么,导致它在 C++17 中被弃用?

根据cppreference.com,std::shared_ptr::unique() 在 C++17 中被弃用为

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

我理解这对于use_count() > 1 是正确的:当我持有对它的引用时,其他人可能同时放开他的或创建一个新副本。

但是如果 use_count() 返回 1(这是我在调用 unique() 时感兴趣的内容),那么没有其他线程可以以一种活泼的方式更改该值,所以我希望这应该是安全的:

if (myPtr && myPtr.unique()) 
    //Modify *myPtr

我自己搜索的结果:

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

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

注意:当多个线程可以影响use_count()的返回值时,应将结果视为近似值。 特别是,use_count() == 1 并不意味着通过先前销毁的shared_ptr 的访问在任何意义上都已完成。 — 结束说明

我知道当前指定 use_count() 的方式可能就是这种情况(由于缺乏保证同步),但为什么解决方案不只是指定此类同步并因此使上述模式安全?如果存在不允许此类同步的基本限制(或使其代价高昂),那么如何正确实现析构函数?

更新:

我忽略了@alexeykuzmin0 和@rubenvb 提出的明显案例,因为到目前为止,我只在其他线程本身无法访问的shared_ptr 实例上使用了unique()。因此,该特定实例不会以不正当的方式被复制。

我仍然有兴趣了解 CA 14 到底是什么,因为我相信我的所有 unique() 用例只要保证与其他 shared_ptr 不同实例发生的任何情况同步就可以工作线程。所以它对我来说似乎仍然是一个有用的工具,但我可能会在这里忽略一些基本的东西。

为了说明我的想法,请考虑以下内容:

class MemoryCache 
public:
    MemoryCache(size_t size)
        : _cache(size)
    
        for (auto& ptr : _cache) 
            ptr = std::make_shared<std::array<uint8_t, 256>>();
        
    

    // the returned chunk of memory might be passed to a different thread(s),
    // but the function is never accessed from two threads at the same time
    std::shared_ptr<std::array<uint8_t,256>> getChunk()
    
        auto it = std::find_if(_cache.begin(), _cache.end(), [](auto& ptr)  return ptr.unique(); );
        if (it != _cache.end()) 
            //memory is no longer used by previous user, so it can be given to someone else
            return *it;
         else 
            return;
        
    
private:
    std::vector<std::shared_ptr<std::array<uint8_t, 256>>> _cache;
;

这有什么问题吗(如果unique()实际上会与其他副本的析构函数同步)?

【问题讨论】:

为什么 1 是特例?在您致电 unique 之后以及在您完成您正在做的任何事情之前,可能会创建另一个副本。 @rubenvb:我的错误 - 唯一的是 const,所以另一个线程可以在没有数据竞争的情况下进行复制 const 与此无关。如果持有 shared_ptr 的对象本身可以从多个线程访问,则无论如何都可以制作 shared_ptr 的副本。 @MikeMB,我正要回答你的更新,但我看到 alexeykuzmin0 的回答中的一句话几乎回答了你的更新。你是对的,如果你的每个线程只使用 shared_ptr 的副本,那么 unique() 就可以正常工作。在您引用的论文和 alexykuzmin0 的答案中,多个线程共享对唯一 shared_ptr 的引用...在 alexykuzmin0 的答案中,句子unique()=true 表示没有人有指向同一内存的 shared_ptr...可能会说几乎一样的话不? 当然use_count 没有被弃用,所以你可以继续使用use_count()==1,只要你记得它是活泼的。 【参考方案1】:

考虑以下代码:

// 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

【讨论】:

谢谢。我不知道,为什么我忽略了那个明显的案例。我仍然不明白为什么要弃用该功能,但至少现在这个决定对我来说更有意义。 我稍微更新了我的问题。如果您对此有任何想法,请随时分享。 我不明白为什么这个答案有这么多赞成票。从未声称s.unique() == true 意味着没有其他线程可以访问s 这很愚蠢。线程 1 可能正在执行修改sanything,这将是一场竞赛。这与 unique 无关,只是因为您真的不希望线程共享 shared_ptr 的实例,而是每个线程都有自己的实例,引用相同的底层对象。【参考方案2】:

我认为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() 之间没有happens-before 关系。 所以我们不能期望关系 [W] ⇒ [D] ⇒ [U] ⇒ [R] 和 [W] ⇒ [R]。 ('⇒' 表示发生前的关系)。


已编辑:我发现了两个相关的 LWG 问题; LWG2434. shared_ptr::use_count() is efficient,LWG2776. 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 删除了 shared_ptruse_count()unique() 的“仅调试”限制引入了一个错误。为了使unique() 产生有用且可靠的值,它需要一个同步子句来确保通过另一个引用的先前访问对于unique() 的成功调用者是可见的。 许多当前的实现使用宽松的负载,并且不提供此保证,因为它没有在标准中说明。对于调试/提示使用没问题。没有它,规范就不清楚并且可能具有误导性。

[...]

我更愿意将use_count() 指定为仅提供实际计数的不可靠提示(另一种表示仅调试的方式)。或者像 JF 建议的那样弃用它。 如果不增加更多的围栏,我们就无法使use_count() 可靠。我们真的不希望有人等待use_count() == 2 来确定另一个线程已经走了那么远。不幸的是,我认为我们目前没有说任何话来明确这是一个错误。

这意味着use_count() 通常使用memory_order_relaxed,并且根据use_count() 既没有指定也没有实现唯一性。

【讨论】:

这实际上是一个有趣的点。任何以您的演示示例的方式使用共享指针的人都可能会被鞭打,但可能会有更复杂的模式,其中这个假设是在某处隐式做出的。我仍然希望他们通过指定同步效果而不是弃用它来修复 unique(),但可能会有我看不到的性能影响。 感谢您挖掘这些 cmets 那是完全错误的:“所以,unique() 成员函数在多线程时将无用”。不,unique() 对同步没有用处。这根本不意味着它是“无用的”。我当然希望你不是真的那个意思,因为那样所有不能用于同步的东西都将“在多线程时毫无用处”。 似乎 [R] 中没有与 [W] 的数据竞争,因为 sp1.unique() == true 仅当 another_thread 完成时。线程使用 sp1 的副本,因此至少存在两个对指针的引用,并且 sp1.unique() 在线程运行时不能为真 @user3514538 您对 C++ 原子的解释是错误的,请阅读LWG2776。【参考方案3】:

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

本文档包含 Issaquah 会议的所有 NB(国家机构)cmets。 CA 14 内容如下:

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

【讨论】:

非常感谢 - 我想知道为什么委员会没有采纳评论中提出的第一个解决方案:“一个解决方案可以让 unique() 使用 memory_order_acquire,并指定引用计数递减操作同步具有唯一性()。” @MikeMB 恐怕我的搜索没有发现任何可能揭示他们动机的东西。这些信息可能存在,但它不是一个容易实现的目标。 为此所需的同步将使一些相当常见的操作(如复制)变得悲观,以启用(ab-)使用unique/use_count,而这本来就不是有意的。 @FabioFracassi:您能在这里详细介绍一下吗?在我的示例中唯一可以使用的 Afaik 引用计数增量不必与任何东西同步(但是减量会)。 @Oktalist:AFAIk,即使是对原子的宽松负载也会保留修改顺序,并且不允许凭空产生值 (en.cppreference.com/w/cpp/atomic/memory_order#Relaxed_ordering)。在线程 3 中的增量和线程 2 中 y 的销毁之间必须存在同步障碍,否则无论如何您都会在 y 上发生数据竞争。因此递增发生在递减之前,因此 ref_count 永远不会低于 2(即使从线程 1 的角度来看也是如此)。【参考方案4】:

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 中删除的原因:具有误导性。

【讨论】:

这同样适用于任何weak_ptr(这是enable_shared_from_this 最终存储的内容),因为unique 不将弱引用计为“使用”。 我不了解连接。仅仅因为对象由 shared_ptr 管理并不意味着它继承自 enable_shared_from_this。 @MikeMB:这是正确的,但我引用 'enable_shared_from_this' 来证明 'unique()' 不能保证任何事情,因为来自 shared_ptr 的 API 可能会以不同的方式改变引用计数器线程,增加或减少。 是的,我的意思是你的论点适用于std::shared_ptr&lt;derived_from_enable_shared_from_this&gt;,但不适用于任何其他shared_ptr&lt;T&gt;

以上是关于为啥不推荐使用 std::shared_ptr::unique() ?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Qt 的 qHash() 没有 std::shared_ptr 的重载?

使用std shared_ptr作为std :: map键

std::shared_ptr 和初始化列表

为啥 一个线程读数据,一个线程写数据,要加锁

使用相同的函数但重载不同的 std::tr1::shared_ptr<T> 和 std::shared_ptr<T>

[C++][原创]std::shared_ptr简单使用