提问人:Elad Maimoni 提问时间:10/13/2023 更新时间:10/14/2023 访问量:140
如果生产者是单线程,依靠“use_count()”来重用“shared_ptr”内存是否安全?
Is it safe to rely on "use_count()" to re-use "shared_ptr" memory in a in case the producer is a single thread?
问:
这与这个悬而未决的问题有点相似,但稍微具体一些。
在我的应用程序中,我有一个生产者线程,它生成对象供其他线程使用。可以安全地假设此线程是唯一创建这些对象的线程。
我希望能够在所有其他消费者线程使用完此类对象后重用它。
最初我以为,如果生产者将保存所有创建的对象,那么如果它们的引用计数为 1,它可以重用它们。但似乎这还不够可靠,无法确定这一点,因为它使用的是弱内存访问(为什么?std::shared_ptr<T>::use_count
我有一个测试应用程序,我注意到有时即使其他消费者已经完成了该对象(或者这可能是我的软件错误),有时仍可能返回> 1。use_count
问题:
- 在这种情况下,依靠等于 1 是否是一种安全的方法?
use_count
- 如果它不安全,为什么?
- 对于这个用例,有哪些更好的替代方案?
答:
use_count
在多线程场景中不可靠,它不是原子的。这真的是超级不可靠。如果在重用之前绝对有必要将 ref 计数为 1,请查看 std::atomic 共享指针,或某种专门用于检查 ref 计数的互斥锁功能。
评论
use_count()
是原子载荷。问题在于它是一个具有宽松内存排序的原子负载,因此不能依赖结果,因为此负载可以任意重新排序。
std::shared_ptr
1
不,这是不安全的。 是一个非常有问题的函数,只能用作粗略的近似值。[util.smartptr.shared.obs] shared_ptr
::use_count中的标准注释:use_count
当多个线程可能影响 的返回值时,结果是近似值。 特别是,并不意味着通过先前销毁的访问在任何意义上都已完成。
use_count()
use_count() == 1
shared_ptr
这些问题是由缺乏同步引起的。在实践中,问题是:
删除 和 in 的“仅调试”限制引入了一个错误:为了生成有用且可靠的值,它需要一个 synchronize 子句来确保通过另一个引用进行的先前访问对 的成功调用者可见。许多当前的实现使用宽松的负载,并且不提供此保证,因为它没有在标准中说明。对于调试/提示使用,这是可以的。没有它,规范是不清楚和误导的。
use_count
unique
shared_ptr
unique
unique
- P0521:针对 CA 14 的拟议决议(shared_ptr
use_count
/unique
)
注意:曾经有一个 unique(
) 函数返回 use_count() == 1
,由于具有误导性而被删除。
您会受到这些问题的影响,即使只有一个线程生成新对象,缺乏同步也会阻止您确定地声明唯一所有权。std::shared_ptr
什么是更好的选择?
在您的例子中,只有一个线程实际生成对象并增加引用计数。
您还可以使该线程听起来像是执行一些后处理,因此永远不会在其他线程之前终止。std::shared_ptr
这听起来像是该线程实际上具有唯一的所有权,并且可以改用。所有其他线程都可以被赋予一个非拥有的原始指针。
为了跟踪有多少线程可以访问指针,您可以使用原子计数器,例如在线程启动/联接时递增/递减(使用 )。std::unique_ptr
std::atomic_int
std::memory_order::acq_rel
您还可以使用 std::latch
或其他一些计数机制来跟踪有多少线程放弃了对指向对象的引用。一旦所有线程都落入闩锁,只有一个线程会访问它。
不应该依赖的主要原因是,一旦你开始检查它,它给你的信息就已经过时了。
(即使它报告这并不意味着它不能异步更改)use_count()
1
这是两个主要的边缘情况,当涉及多个线程时,它们实际上毫无用处:use_count()
1.是一回事std::weak_pointer<T>
考虑两个线程,一个线程是 a 到你的对象,另一个是 a 到同一个对象:std::shared_ptr<T>
std::weak_ptr<T>
// thread 1
std::shared_ptr<T> ptr = /* ... */;
// ...
long count = ptr.use_count();
if(count == 1)
reuse(ptr);
// thread 2
std::weak_ptr<T> wptr = /* ... */;
// ...
std::shared_ptr<T> ptr = wptr.lock();
if(ptr)
fiddle(ptr);
这些可以像这样交错:
线程 1 | 线程 2 |
---|---|
long count = ptr.use_count(); (计数将为 1) |
|
std::shared_ptr<T> ptr = wptr.lock(); (将成功) |
|
if(count == 1) reuse(ptr); (true,调用重用) |
|
if(ptr) fiddle(ptr); (true,调用小提琴) |
...然后,线程 1 试图重用您的对象,认为它具有唯一的强引用,而线程 2 使用新创建的强引用来摆弄它。
2. 最后一个实例可以共享std::shared_ptr<T>
如果最后一个实例在两个线程之间共享,您还可以获得争用条件:std::shared_ptr<T>
// global variable
static std::shared_ptr<T> gptr = /* ... */
// thread 1
long count = gptr.use_count();
if(count == 1)
reuse(gptr);
// thread 2
std::shared_ptr<T> ptr = gptr;
fiddle(ptr);
这些可以像这样交错:
线程 1 | 线程 2 |
---|---|
long count = ptr.use_count(); (计数将为 1) |
|
std::shared_ptr<T> ptr = gptr; (创建第二个强引用) |
|
if(count == 1) reuse(gptr); (true,调用重用) |
|
fiddle(ptr); (调用小提琴) |
...这又会导致两者在不同的线程上被调用相同的对象。reuse
fiddle
回答您的问题
-
在这种情况下,依靠use_count等于 1 是否是一种安全的方法?
如果它不安全,为什么?只有在以下情况下才安全:
- 您检查
use_count() == 1
- 您确定不存在指向该值的弱指针
- 您确信最后剩下的内容只能由执行检查的线程访问
std::shared_ptr<T>
use_count()
- 您检查
-
对于这个用例,有哪些更好的替代方案?
在 上使用自定义删除器,
或者(如果可以使用 C++ 11 以上的 C++ 版本,特别是 C++20)为您的类型实现销毁删除解除操作函数。(运算符删除
- 参见 (27) - (30))std::shared_ptr
请注意,对于这两个选项,您都不应该使用 ,因为这会将控制块与对象一起分配,如果您以后想重用它,则要避免这样做。
std::make_shared
-
因为它使用弱内存访问(为什么?
即使它是强内存访问,结果也不可靠。
返回后,任何其他线程都可能锁定 a,从而更改引用计数。use_count()
std::weak_ptr
因此,唯一可以有意义地使用 的值的情况是当它返回 1 并且最后一个实例不在线程之间共享并且没有 s 在起作用时。
在这种情况下,是否重新排序加载并不重要,因为无论如何只能有 1 个线程。use_count()
std::shared_ptr
std::weak_ptr
use_count()
评论
use_count() == 1
std::shared_ptr
std::mutex
std::latch
use_count() == 1
std::memory_order::acq_rel
)
std::shared_ptr
评论
use_count
std::weak_ptr
shared_ptr