std::shared_ptr<T>.use_counter() 的问题

Woes with std::shared_ptr<T>.use_counter()

https://en.cppreference.com/w/cpp/memory/shared_ptr/use_count 状态:

In multithreaded environment, the value returned by use_count is approximate (typical implementations use a memory_order_relaxed load)

但这是否意味着 use_count() 在多线程环境中完全没有用?

考虑以下示例,其中 Circular class 实现了 std::shared_ptr<int>.

的循环缓冲区

提供给用户一个方法——get(),它检查next元素在std::array<std::shared_ptr<int>>中的引用计数是否大于1(这是我们不想要的) ,因为这意味着它由之前调用 get()).

的用户持有

如果是 <= 1,将向用户返回 std::shared_ptr<int> 的副本。

在这种情况下,用户是两个除了喜欢在循环缓冲区上调用 get() 之外什么都不做的线程 - 这就是他们的生活目的。

当我执行程序时实际发生的情况是它运行了几个周期(通过向循环缓冲区class添加一个counter进行测试),之后它抛出异常,抱怨下一个元素的引用计数器是 > 1.

这是在多线程环境下use_count()返回的值是近似值的说法的结果吗?

是否可以调整底层机制以使其具有确定性并按照我希望的方式运行?

如果我的想法是正确的 - 在 get() 函数中 next 元素的 use_count()(或者更确切地说是实际用户数)永远不会超过 1 =15=],因为只有两个消费者,每次一个线程调用get(),它已经释放了它旧的(复制的)std::shared_ptr<int>(这反过来意味着剩余的std::shared_ptr<int>驻留在 Circular::ints_ 中的引用计数应该仅为 1)。

#include <mutex>
#include <array>
#include <memory>
#include <exception>
#include <thread>

class Circular {
    public:
      Circular() {
          for (auto& i : ints_) { i = std::make_shared<int>(0); }
      }

      std::shared_ptr<int> get() {
        std::lock_guard<std::mutex> lock_guard(guard_);
        index_ = index_ % 2;  // Re-set the index pointer.

        if (ints_.at(index_).use_count() > 1) {
          // This shouldn't happen - right? (but it does)
          std::string excp = std::string("OOPSIE: ") + std::to_string(index_) + " " + std::to_string(ints_.at(index_).use_count());
          throw std::logic_error(excp);
        }

        return ints_.at(index_++);
      }

    private:
        std::mutex                          guard_;
        unsigned int                        index_{0};
        std::array<std::shared_ptr<int>, 2> ints_;
};

Circular circ;
void func() {
    do {
      auto scoped_shared_int_pointer{circ.get()};
    }while(1);
}

int main() {
  std::thread t1(func), t2(func);

  t1.join(); t2.join();
}

虽然 use_count 充满了问题,但现在的核心问题不在逻辑之内。

假设线程 t1 在索引 0 处获取 shared_ptr,然后 t2t1 完成其第一次循环迭代之前运行其循环两次。 t2 将获取索引 1 处的 shared_ptr,释放它,然后尝试获取索引 0 处的 shared_ptr,并且会触发失败条件,因为 t1 只是运行落后。

现在,也就是说,在更广泛的上下文中,它并不是特别安全,就像用户创建一个weak_ptr一样,use_count完全有可能从1到2而不通过通过这个功能。在这个简单的例子中,让它循环遍历索引数组直到找到空闲的共享指针。

use_count 仅用于调试,不应使用。如果您想知道什么时候其他人不再引用指针,只需让共享指针消失并使用自定义删除器检测它,然后对现在未使用的指针做任何您需要做的事情。

这是您如何在代码中实现它的示例:

#include <mutex>
#include <array>
#include <memory>
#include <exception>
#include <thread>
#include <vector>
#include <iostream>

class Circular {
public:
  Circular() {
    size_t index = 0;
    for (auto& i : ints_)
    {
      i = 0;
      unused_.push_back(index++);
    }
  }

  std::shared_ptr<int> get() {
    std::lock_guard<std::mutex> lock_guard(guard_);

    if (unused_.empty())
    {
      throw std::logic_error("OOPSIE: none left");
    }
    size_t index = unused_.back();
    unused_.pop_back();
    return std::shared_ptr<int>(&ints_[index], [this, index](int*) {
      std::lock_guard<std::mutex> lock_guard(guard_);
      unused_.push_back(index);
    });
  }

private:
  std::mutex guard_;
  std::vector<size_t> unused_;
  std::array<int, 2> ints_;
};

Circular circ;
void func() {
  do {
    auto scoped_shared_int_pointer{ circ.get() };
  } while (1);
}

int main() {
  std::thread t1(func), t2(func);

  t1.join(); t2.join();
}

保留未使用索引的列表,当共享指针被销毁时自定义删除器returns索引返回未使用索引列表,准备在下一次调用get中使用.