【问题标题】:Is there a non-atomic equivalent of std::shared_ptr? And why isn't there one in <memory>?是否有 std::shared_ptr 的非原子等价物?为什么 <memory> 中没有一个?
【发布时间】:2013-02-14 06:34:09
【问题描述】:

这是一个由两部分组成的问题,都是关于std::shared_ptr 的原子性:

1. 据我所知,std::shared_ptr&lt;memory&gt; 中唯一的原子智能指针。我想知道是否有可用的std::shared_ptr 的非原子版本(我在&lt;memory&gt; 中看不到任何东西,所以我也愿意接受标准之外的建议,比如Boost 中的建议)。我知道boost::shared_ptr 也是原子的(如果未定义BOOST_SP_DISABLE_THREADS),但也许还有另一种选择?我正在寻找与std::shared_ptr 具有相同语义但没有原子性的东西。

2. 我明白为什么std::shared_ptr 是原子的;这有点好。然而,它并不适用于所有情况,C++ 历来有“只为你使用的东西付费”的口头禅。如果我不使用多个线程,或者如果我使用多个线程但没有跨线程共享指针所有权,那么原子智能指针就过分了。我的第二个问题是为什么 C++11 中没有提供std::shared_ptr 的非原子版本? (假设有一个为什么)(如果答案只是“根本没有考虑过非原子版本”或“从来没有人要求过非原子版本”,那很好!)。

关于问题 #2,我想知道是否有人曾提出过 shared_ptr 的非原子版本(向 Boost 或标准委员会提出)(不是要取代 shared_ptr 的原子版本,而是与它)并且由于特定原因被击落。

【问题讨论】:

  • 您在这里究竟关心什么“成本”?原子递增整数的成本?对于任何实际应用,这实际上是您关心的成本吗?还是您只是过早地优化?
  • @NicolBolas:好奇心比什么都重要;我(目前)没有任何代码/项目我非常想使用非原子共享指针。但是,我有一些项目(过去),其中 Boost 的 shared_ptr 由于其原子性而显着放缓,并且定义 BOOST_DISABLE_THREADS 产生了明显的差异(我不知道 std::shared_ptr 是否会有相同的成本boost::shared_ptr 拥有)。
  • @Close voters:问题的哪一部分没有建设性?如果第二个问题没有具体的 why ,那很好(一个简单的“根本没有考虑过”将是一个足够有效的答案)。我很好奇是否存在特定的原因/理由。我会说,第一个问题肯定是一个有效的问题。如果我需要澄清这个问题,或者对它做一些细微的调整,请告诉我。但我看不出它有什么建设性。
  • @Cornstalks 好吧,这可能只是人们对他们很容易将其视为“过早优化”而忽略的问题反应不佳,无论它们多么有效,多么恰当或相关的问题是,我猜。我自己认为没有任何理由将其关闭为非建设性的。
  • (不能写答案现在它已经关闭,所以评论)当你的程序不使用多线程时使用 GCC shared_ptr 不使用原子操作作为引用计数。请参阅gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html 处的 (2) 以获取 GCC 的补丁,以允许即使在多线程应用程序中也可以使用非原子实现,用于线程之间不共享的 shared_ptr 对象。我多年来一直坐在那个补丁上,但我正在考虑最终将它提交给 GCC 4.9

标签: c++ c++11 smart-pointers


【解决方案1】:

1。我想知道是否有非原子版本的 std::shared_ptr 可用

标准未提供。 “第 3 方”库很可能提供了一个。事实上,在 C++11 和 Boost 之前,似乎每个人都编写了自己的引用计数智能指针(包括我自己)。

2。我的第二个问题是为什么 C++11 中没有提供 std::shared_ptr 的非原子版本?

这个问题在 2010 年的 Rapperswil 会议上进行了讨论。该主题是由瑞士的国家机构第 20 号意见提出的。辩论的双方都有强烈的论据,包括你在问题中提供的论据。然而,在讨论结束时,绝大多数(但不是一致)投票反对添加shared_ptr 的非同步(非原子)版本。

反对的论点:

  • 使用不同步的 shared_ptr 编写的代码最终可能会在以后的线程代码中使用,最终导致难以调试的问题而没有警告。

  • 拥有一个“通用”shared_ptr 是引用计数中流量的“一种方式”有好处:来自the original proposal

    无论使用何种功能,都具有相同的对象类型,极大地促进了库之间的互操作性,包括第三方库。

  • 原子的成本虽然不是零,但也不是压倒性的。通过使用不需要使用原子操作的移动构造和移动分配来减轻成本。此类操作常用于vector&lt;shared_ptr&lt;T&gt;&gt;erase和insert。

  • 没有什么可以禁止人们编写自己的非原子引用计数智能指针,如果这确实是他们想要做的。

那天在拉珀斯维尔的 LWG 的最后一句话是:

拒绝 CH 20。目前没有达成更改的共识。

【讨论】:

  • 哇,完美,感谢您提供的信息!这正是我希望找到的信息。
  • > Has the same object type regardless of features used, greatly facilitating interoperability between libraries, including third-party libraries. 这是一个非常奇怪的推理。第三方库无论如何都会提供他们自己的类型,那么如果他们以 std::shared_ptr、std::non_atomic_shared_ptr 等的形式提供它又有什么关系呢?无论如何,您都必须使代码适应库返回的内容
  • 就特定于库的类型而言确实如此,但其想法是,标准类型也出现在第三方 API 中的很多地方。例如,我的图书馆可能会在某处使用std::shared_ptr&lt;std::string&gt;。如果其他人的库也采用这种类型,调用者可以将相同的字符串传递给我们双方,而无需在不同表示之间进行转换的不便或开销,这对每个人来说都是一个小小的胜利。
  • @Jean-MichaëlCelerier 另外,想象一个在其界面中使用std::unsynchronized_shared_ptr 的库。您想使用该库,但您的应用程序恰好是多线程的。你现在有了一个指针,每次传递它时都必须用互斥锁保护它。
  • @LukasBarth 接口中std::unsynchronized_shared_ptr 的替代如果后者不可用是not std::shared_ptr,它是让我们使用我们自己的自定义共享指针类型这正是我们想要的,即使它需要显式同步。让最终用户的情况变得更糟。
【解决方案2】:

Howard 已经很好地回答了这个问题,Nicol 提出了一些关于使用单一标准共享指针类型而不是许多不兼容的共享指针类型的好处。

虽然我完全同意委员会的决定,但我确实认为在特殊情况下使用类似 shared_ptr 的非同步类型会有一些好处,因此我已经调查了几次该主题.

如果我不使用多个线程,或者如果我使用多个线程但没有跨线程共享指针所有权,那么原子智能指针就过分了。

当您的程序不使用多个线程时,使用 GCC shared_ptr 不使用原子操作作为引用计数。这是通过检测程序是否为多线程的包装函数更新引用计数来完成的(在 GNU/Linux 上,这是通过检查 Glibc 中的一个特殊变量来完成的,该变量表示程序是否为单线程[1] sup>) 并相应地分派给原子或非原子操作。

多年前我意识到,由于 GCC 的 shared_ptr&lt;T&gt; 是根据 __shared_ptr&lt;T, _LockPolicy&gt; base class 实现的,因此即使在多线程代码中,也可以通过显式使用 __shared_ptr&lt;T, __gnu_cxx::_S_single&gt; 来使用具有单线程锁定策略的基类。您可以使用这样的别名模板来定义不是线程安全但速度稍快的共享指针类型[2]:

template<typename T>
  using shared_ptr_unsynchronized = std::__shared_ptr<T, __gnu_cxx::_S_single>;

这种类型不能与std::shared_ptr&lt;T&gt; 互操作,并且只有在保证shared_ptr_unsynchronized 对象在没有用户提供的额外同步的情况下永远不会在线程之间共享时才能安全使用。

这当然是完全不可移植的,但有时没关系。如果shared_ptr_unsynchronized&lt;T&gt;shared_ptr&lt;T&gt; 的别名,使用正确的预处理器hack,您的代码仍然可以在其他实现中正常工作,而使用GCC 会更快一些。


[1] 在 Glibc 2.33 添加该变量之前,包装函数会检测程序是否链接到 libpthread.so,作为检查单线程与多线程的不完美方法。

[2] 不幸的是,因为这不是一个预期的用例,它在 GCC 4.9 之前并没有以最佳方式工作,即使您明确请求了 `_S_single` 策略,一些操作仍然使用包装函数,因此被分派给原子操作.请参阅http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html 的第 (2) 点以了解更多详细信息以及 GCC 的补丁,以允许即使在多线程应用程序中也可以使用非原子实现。我在那个补丁上坐了很多年,但我最终为 GCC 4.9 提交了它。

【讨论】:

  • 只是想知道,您的模板别名示例中是否有错字? IE。我认为它应该读为 shared_ptr_unsynchronized = std::__shared_ptr
  • 很棒的细节!最近我遇到了一个问题,如this question 中所述,最终让我查看了std::shared_ptrstd::__shared_ptr__default_lock_policy 等的源代码。这个答案证实了我从代码中理解的内容。
【解决方案3】:

我的第二个问题是为什么 C++11 中没有提供 std::shared_ptr 的非原子版本? (假设有一个原因)。

人们可以很容易地问为什么没有侵入式指针,或者共享指针的任何其他可能变体。

shared_ptr 的设计是从 Boost 传下来的,旨在创建智能指针的最低标准通用语言。一般来说,你可以把它从墙上拉下来并使用它。它可以在各种应用程序中普遍使用。你可以把它放在一个界面中,很有可能好人会愿意使用它。

线程只会在未来变得更多流行。事实上,随着时间的推移,线程通常将成为实现性能的主要手段之一。要求基本的智能指针完成支持线程所需的最低限度的工作有助于实现这一目标。

将六个具有细微差异的智能指针转储到标准中,或者更糟糕的是,将基于策略的智能指针转储到标准中,这将是可怕的。每个人都会选择他们最喜欢的指针并放弃所有其他指针。没有人能够与其他人交流。就像当前使用 C++ 字符串的情况一样,每个人都有自己的类型。更糟糕的是,因为与字符串的互操作比智能指针类之间的互操作要容易得多。

Boost,以及委员会的扩展,选择了一个特定的智能指针来使用。它提供了良好的功能平衡,并且在实践中被广泛和普遍使用。

std::vector 在某些极端情况下也比裸数组效率低下。它有一些限制;有些用途确实希望对vector 的大小有一个硬性限制,而不使用抛出分配器。然而,委员会并没有将vector 设计为适合所有人。它被设计为大多数应用程序的良好默认设置。那些无法使用的人可以编写一个适合他们需求的替代方案。

如果shared_ptr 的原子性是一种负担,就像你可以使用智能指针一样。再说一次,人们也可以考虑不要过多地复制它们。

【讨论】:

  • +1 表示“人们也可以考虑不要过多地复制它们。”
  • 如果你曾经连接过探查器,那么你就很特别,你可以像上面那样调出参数。如果您没有难以满足的操作要求,则不应使用 C++。像你这样争论是让 C++ 被任何对高性能或低延迟感兴趣的人普遍唾弃的好方法。这就是游戏程序员不使用 STL、boost 甚至异常的原因。
  • 为清楚起见,我认为答案顶部的引用应为“为什么 C++ 中没有提供 std::shared_ptr 的 非原子 版本11 点?”
【解决方案4】:

Boost 提供了一个非原子的shared_ptr。它叫local_shared_ptr,可以在boost的智能指针库中找到。

【讨论】:

  • +1 是一个简短可靠的回复,有很好的引用,但是这种指针类型看起来很昂贵 - 在内存和运行时方面,由于一个额外的间接级别(local->shared->ptr vs共享->ptr)。
  • @Red.Wave 你能解释一下间接的含义以及它如何影响性能吗?你的意思是它是一个带有柜台的shared_ptr,即使它是本地的?或者你的意思是它还有另一个问题?文档说 唯一的 区别在于这不是原子的。
  • 每个本地 ptr 都会对原始共享 ptr 进行计数和引用。因此,对最终指针对象的任何访问都需要从本地到共享 ptr 的解引用,然后对被指针对象进行解引用。因此,还有一个间接叠加到来自共享 ptr 的间接。这会增加开销。
  • @Red.Wave 您从哪里获得这些信息?这:“因此,对最终指针的任何访问都需要从本地到共享 ptr 的取消引用”需要一些引用。我在 boost 文档中找不到。同样,我在文档中看到的是它说 local_shared_ptrshared_ptr 除了原子之外是相同的。我真的很想知道您所说的是否属实,因为我在需要高性能的应用程序中使用local_shared_ptr
  • @Red.Wave 如果您查看实际的源代码github.com/boostorg/smart_ptr/blob/…,您会发现没有双重间接。文档中的这一段只是一个心智模型。
【解决方案5】:

我正在准备工作中关于 shared_ptr 的演讲。我一直在使用修改后的 boost shared_ptr,避免使用单独的 malloc(就像 make_shared 可以做的那样)和用于锁定策略的模板参数,比如上面提到的 shared_ptr_unsynchronized。我正在使用来自

的程序

http://flyingfrogblog.blogspot.hk/2011/01/boosts-sharedptr-up-to-10-slower-than.html

作为测试,在清理了不必要的 shared_ptr 副本之后。该程序仅使用主线程并显示测试参数。测试环境是一个运行 linuxmint 14 的笔记本。这里是以秒为单位的时间:

测试运行设置 boost(1.49) 标准与 make_shared 修改提升 mt-unsafe(11) 11.9 9/11.5(-pthread on) 8.4 原子(11) 13.6 12.4 13.0 mt-unsafe(12) 113.5 85.8/108.9(-pthread on) 81.5 原子(12) 126.0 109.1 123.6

只有 'std' 版本使用 -std=cxx11,而 -pthread 可能会在 g++ __shared_ptr 类中切换 lock_policy。

从这些数字中,我看到了原子指令对代码优化的影响。测试用例不使用任何 C++ 容器,但如果对象不需要线程保护,vector&lt;shared_ptr&lt;some_small_POD&gt;&gt; 可能会受到影响。 Boost 受到的影响较小,因为额外的 malloc 限制了内联和代码优化的数量。

我还没有找到一台具有足够内核的机器来对原子指令的可扩展性进行压力测试,但仅在必要时使用 std::shared_ptr 可能会更好。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-11-03
    • 1970-01-01
    • 2016-11-29
    • 1970-01-01
    • 1970-01-01
    • 2016-01-19
    • 1970-01-01
    相关资源
    最近更新 更多