【问题标题】:Fully thread-safe shared_ptr implementation完全线程安全的 shared_ptr 实现
【发布时间】:2010-10-01 07:20:59
【问题描述】:

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

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

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

奇怪的是 boost 文档这么说:

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

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

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

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

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

【问题讨论】:

  • "同时写入一个普通指针是线程安全的" -- 你确定吗?
  • 至少在x86上,如果指针对齐正确,写操作是原子的。
  • 在一个线程中同时写入并在另一个线程中删除呢? (delete 本质上是一种特殊的写入方式;删除所指向的项目)。
  • Max:您所描述的是同时读取和写入。 delete 不会改变指针变量本身的值,因此它不算作写入——它是(可能)被写入的 pointed-to 值(如果说是由析构函数写入的)存在)。
  • "写操作是原子的" 这并不是常识中的“线程安全”。

标签: c++ boost thread-safety shared-ptr


【解决方案1】:

为这种完全线程安全的 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 上它可能非常昂贵。更重要的是,你也在忙着等待。你可以开始使用 futex 之类的东西变得聪明——但到那时你已经重新发明了锁。

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

【讨论】:

  • 很好的答案,谢谢。这就是我一直在寻找的,即这意味着什么。我猜你的例子在没有 B 的情况下也能工作?
  • 实际上,它仍然会仅与线程 1 和 3 中断 - 添加线程 2 只是为了表明我们已经先用某些东西对其进行了初始化。
  • “添加障碍会影响性能”!!好吧,那么您应该知道现在在 boost 和 std::shared (base_ref_cnt 类)中都存在障碍。因为他们使用原子,并且原子确实在 cpu(内存栅栏)上执行必要的加载/存储刷新。编译器和 C++11 的 atomic 的内在函数支持甚至比 CPU 栅栏更差,它也是一个编译器栅栏(没有重新排序通过原子的加载/存储)。 shared_ptr 今天已经很慢了。更糟糕的是,它们不是线程安全的。只有在没有弱引用时它们才是安全的。
  • @curiousguy 因为如果不使用互斥锁,您将无法自动更新 2 个计数器。而且他们只使用原子交换/inc/dec,所以我觉得肯定有可能打破内部不变量。我实现了自定义shared_ptr 2 次,但我无法解决这个问题。也许std 的实现者是天才,但有一天我需要检查一下他们是如何做到的。
  • @v.oddou 你能举一个不安全代码的具体例子吗?
【解决方案2】:

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

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

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

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

【讨论】:

  • 我同意两个处理器可以看到不同的内置指针值(即看不到另一个处理器完成的更新),但这对于应用程序来说可能是可以接受的。我想这不是最严格意义上的线程安全,但它至少不会崩溃(boost shared_ptr 会双重删除)。
【解决方案3】:

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

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

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

【讨论】:

  • “我希望你的意思是先将全局共享指针分配给本地共享指针”——是的,我应该使用更好的措辞
【解决方案4】:

您可以使用此实现Atomic Reference Counting Pointers 至少实现引用计数机制。

【讨论】:

    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

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

        【讨论】:

          【解决方案8】:

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

          Boost atomic 用法示例:

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

          【讨论】:

            【解决方案9】:

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

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

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

            【讨论】:

            • 不幸的是,您的代码也不是线程安全的。在intrusive_ptr_release() 中,线程可以在if 的条件评估之后但在delete 之前被抢占。然后另一个线程可以例如调用intrusive_ptr_add_ref()intrusive_ptr_release() 并删除原始线程前面的ptr,然后继续,就好像什么都没发生一样并尝试再次删除该ptr。
            猜你喜欢
            • 2013-01-07
            • 2019-06-07
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-09-05
            相关资源
            最近更新 更多