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

Why is std::shared_ptr::unique() deprecated?

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

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

this function is deprecated as of C++17 because use_count is only an approximation in multi-threaded environment.

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

但是如果 use_count() returns 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,但我找不到该评论本身.

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

Note: When multiple threads can affect the return value of use_count(), the result should be treated as approximate. In particular, use_count() == 1 does not imply that accesses through a previously destroyed shared_ptr have in any sense completed. — end note

我知道这可能是当前指定方式 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()实际上会与其他副本的析构函数同步)?

考虑以下代码:

// 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或参考资料。

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

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

The removal of the "debug only" restriction for use_count() and unique() in shared_ptr introduced a bug: in order for unique() to produce a useful and reliable value, it needs a synchronize clause to ensure that prior accesses through another reference are visible to the successful caller of unique(). Many current implementations use a relaxed load, and do not provide this guarantee, since it's not stated in the Standard. For debug/hint usage that was OK. Without it the specification is unclear and misleading.

我认为 P0521R0 通过滥用 shared_ptr 作为线程间同步来解决 潜在的 数据竞争。 它说 use_count() returns 不可靠的引用计数值,因此, 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() 没有任何同步效果,happens-before 关系来自 [D] shared_ptr 的析构函数到 [U] 呼叫 unique()。 所以我们不能期待 [W] ⇒ [D] ⇒ [U] ⇒ [R] 和 [W] ⇒ [R] 的关系。 ('⇒' 表示 happens-before 关系)。


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

LWG2434 引用(强调我的):

shared_ptr and weak_ptr have Notes that their use_count() might be inefficient. This is an attempt to acknowledge reflinked implementations (which can be used by Loki smart pointers, for example). However, there aren't any shared_ptr implementations that use reflinking, especially after C++11 recognized the existence of multithreading. Everyone uses atomic refcounts, so use_count() is just an atomic load.

LWG2776 引用(强调我的):

The removal of the "debug only" restriction for use_count() and unique() in shared_ptr by LWG 2434 introduced a bug. In order for unique() to produce a useful and reliable value, it needs a synchronize clause to ensure that prior accesses through another reference are visible to the successful caller of unique(). Many current implementations use a relaxed load, and do not provide this guarantee, since it's not stated in the standard. For debug/hint usage that was OK. Without it the specification is unclear and probably misleading.

[...]

I would prefer to specify use_count() as only providing an unreliable hint of the actual count (another way of saying debug only). Or deprecate it, as JF suggested. We can't make use_count() reliable without adding substantially more fencing. We really don't want someone waiting for use_count() == 2 to determine that another thread got that far. And unfortunately, I don't think we currently say anything to make it clear that's a mistake.

This would imply that use_count() normally uses memory_order_relaxed, and unique is neither specified nor implemented in terms of use_count().

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