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
,然后 t2
在 t1
完成其第一次循环迭代之前运行其循环两次。 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
中使用.
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
,然后 t2
在 t1
完成其第一次循环迭代之前运行其循环两次。 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
中使用.