13

有人知道完全线程安全的shared_ptr实现吗?例如 boost 的实现shared_ptr对于目标(引用计数)是线程安全的,并且对于同时shared_ptr实例读取也是安全的,但对于写入或读/写则不是。

(参见Boost 文档,示例 3、4 和 5)。

shared_ptr是否存在对实例完全线程安全的 shared_ptr 实现?

奇怪的是 boost 文档说:

shared_ptr 对象提供与内置类型相同级别的线程安全。

但是,如果将普通指针(内置类型)与 进行比较smart_ptr,则同时写入普通指针是线程安全的,但同时写入 asmart_ptr则不是。

编辑:我的意思是 x86 架构上的无锁实现。

EDIT2:这种智能指针的一个示例用例是有许多工作线程使用它们当前的工作项更新全局 shared_ptr 和一个对工作项进行随机采样的监视器线程。shared-ptr 将拥有该工作项,直到另一个工作项指针被分配给它(从而破坏先前的工作项)。监视器将通过将工作项分配给它自己的 shared-ptr 来获得工作项的所有权(从而防止工作项被销毁)。可以通过 XCHG 和手动删除来完成,但如果 shared-ptr 可以做到这一点会很好。

另一个例子是全局 shared-ptr 拥有一个“处理器”,并由某个线程分配,并由某个其他线程使用。当“用户”线程看到处理器 shard-ptr 为 NULL 时,它使用一些替代逻辑来进行处理。如果它不是 NULL,它会通过将处理器分配给它自己的 shared-ptr 来防止处理器被破坏。

4

9 回答 9

18

为这种完全线程安全的 shared_ptr 实现添加必要的障碍可能会影响性能。考虑以下比赛(注意:伪代码比比皆是):

线程 1:global_ptr = A;

线程 2: global_ptr = B;

线程 3:local_ptr = global_ptr;

如果我们把它分解成它的组成操作:

线程 1:

A.refcnt++;
tmp_ptr = exchange(global_ptr, A);
if (!--tmp_ptr.refcnt) delete tmp_ptr;

线程 2:

B.refcnt++;
tmp_ptr = exchange(global_ptr, B);
if (!--tmp_ptr.refcnt) delete tmp_ptr;

线程 3:

local_ptr = global_ptr;
local_ptr.refcnt++;

显然,如果线程 3 在 A 的交换之后读取指针,然后 B 在引用计数可以增加之前删除它,就会发生不好的事情。

为了处理这个问题,我们需要在线程 3 进行 refcnt 更新时使用一个虚拟值:(注意:compare_exchange(variable, expected, new) 如果变量中的值当前等于 new,则自动将变量中的值替换为 new,然后返回 true如果它成功了)

线程 1:

A.refcnt++;
tmp_ptr = global_ptr;
while (tmp_ptr == BAD_PTR || !compare_exchange(global_ptr, tmp_ptr, A))
    tmp_ptr = global_ptr;
if (!--tmp_ptr.refcnt) delete tmp_ptr;

线程 2:

B.refcnt++;
while (tmp_ptr == BAD_PTR || !compare_exchange(global_ptr, tmp_ptr, A))
    tmp_ptr = global_ptr;
if (!--tmp_ptr.refcnt) delete tmp_ptr;

线程 3:

tmp_ptr = global_ptr;
while (tmp_ptr == BAD_PTR || !compare_exchange(global_ptr, tmp_ptr, BAD_PTR))
    tmp_ptr = global_ptr;
local_ptr = tmp_ptr;
local_ptr.refcnt++;
global_ptr = tmp_ptr;

您现在必须在 /read/ 操作的中间添加一个循环,其中包含原子。这不是一件好事——在某些 CPU 上它可能非常昂贵。更重要的是,你也在忙着等待。你可以开始使用 futexes 和诸如此类的东西变得聪明——但到那时你已经重新发明了锁。

这个成本必须由每个操作承担,并且本质上与锁给您的成本非常相似,这就是为什么您通常看不到这种线程安全的 shared_ptr 实现的原因。如果您需要这样的东西,我建议将互斥锁和 shared_ptr 包装到一个便利类中以自动锁定。

于 2009-05-21T01:30:01.323 回答
2

同时写入内置指针当然不是线程安全的。如果你真的想把自己逼疯(例如,你可能有两个线程认为同一个指针有不同的值),那么考虑写入相同值对内存屏障的影响。

回复:评论 - 内置不是双重删除的原因是因为它们根本没有删除(并且我使用的 boost::shared_ptr 的实现不会双重删除,因为它使用特殊的原子增量和减量,所以它只会单次删除,但结果可能会有一个指针和另一个的引用计数。或者两者的几乎任何组合。这会很糟糕。)。boost 文档中的声明是正确的,你得到与内置相同的保证。

RE:EDIT2-您描述的第一种情况在使用内置函数和 shared_ptrs 之间非常不同。在一个(XCHG 和手动删除)中没有引用计数;当你这样做时,你假设你是唯一的所有者。如果使用共享指针,您是说其他线程可能拥有所有权,这使事情变得更加复杂。我相信通过比较和交换是可能的,但这将是非常不便携的。

C++0x 推出了一个原子库,它应该使编写通用多线程代码变得更加容易。您可能必须等到它出来才能看到线程安全智能指针的良好跨平台参考实现。

于 2009-01-14T03:58:13.147 回答
1

我不知道这样的智能指针实现,但我不得不问:这种行为怎么会有用?我能想到的唯一可以同时发现指针更新的场景是竞争条件(即错误)。

这不是批评——很可能有一个合法的用例,我只是想不出。请告诉我!

回复:EDIT2 感谢您提供几个场景。听起来原子指针写入在这些情况下会很有用。(一件小事:对于第二个例子,当你写“如果它不是 NULL,它通过将它分配给它自己的 shared-ptr 来防止处理器被破坏”,我希望你的意思是你将全局共享指针分配给首先本地共享指针,然后检查本地共享指针是否为 NULL - 您描述它的方式容易出现竞争条件,即全局共享指针在您对其进行测试之后变为 NULL,然后再将其分配给本地共享指针。)

于 2009-01-14T06:57:53.830 回答
0

您可以使用此实现原子引用计数指针来至少实现引用计数机制。

于 2010-05-04T19:41:06.227 回答
-1

您的编译器可能已经在较新的 C++ 标准中提供了线程安全的智能指针。我相信TBB正计划添加一个智能指针,但我认为它还没有被包括在内。不过,您也许可以使用 TBB 的线程安全容器之一。

于 2009-01-14T08:39:31.457 回答
-1

您可以通过在每个共享指针中包含一个互斥对象并使用锁包装递增/递减命令来轻松地做到这一点。

于 2009-05-21T01:45:46.487 回答
-1

我不认为这很容易,用 CS 包装你的 sh_ptr 类是不够的。确实,如果您为所有共享指针维护一个单一的 CS,它可以确保避免不同线程之间相互访问和删除 sh_ptr 对象。但这会很糟糕,每个共享指针的一个 CS 对象将是一个真正的瓶颈。如果每个可包装的新 ptr -s 具有不同的 CS ' 将是合适的,但是这样我们应该动态地创建我们的 CS,并确保 sh_ptr 类的复制 ctor 来传输这个共享的 C。现在我们遇到了同样的问题:谁来保证这个 Cs ptr 是否已经被删除。我们可以更聪明地使用每个实例的 volatile m_bReleased 标志,但这样我们就不能在检查标志和使用共享 Cs 之间留下安全漏洞。我可以' 看不到这个问题的完全安全的解决方案。也许那个可怕的全局 Cs 会像杀死应用程序那样是次要的坏事。(对不起我的英语不好)

于 2009-09-11T13:48:34.307 回答
-1

这可能不是您想要的,但boost::atomic文档提供了一个示例,说明如何将原子计数器与intrusive_ptr. intrusive_ptr是 Boost 智能指针之一,它执行“侵入式引用计数”,这意味着计数器“嵌入”在目标中,而不是由智能指针提供。

提升atomic使用示例:

http://www.boost.org/doc/html/atomic/usage_examples.html

于 2013-03-16T03:17:51.547 回答
-3

在我看来,最简单的解决方案是使用 aintrusive_ptr并进行一些小的(但必要的)修改。

我在下面分享了我的实现:

http://www.philten.com/boost-smartptr-mt/

于 2011-01-17T09:24:40.457 回答