对象的共享所有权是不良设计的标志吗?

时间:2009-12-25 03:11:07

标签: c++ garbage-collection raii

背景:在阅读Dr. Stroustrup's papers和常见问题解答时,我会注意到传奇的CS科学家和程序员提出的一些强烈的“意见”和建议。其中一个是关于C ++ 0x中的shared_ptr。他开始解释shared_ptr以及它如何表示指向对象的共享所有权。在最后一行,he says and I quote

  

shared_ptr代表共享   所有权但共享所有权不是   我的理想:如果一个物体更好   有明确的所有者和明确的,   可预测的寿命。

我的问题:RAII在多大程度上取代了垃圾收集等其他设计模式?我假设手动内存管理不用于表示系统中的共享所有权。

4 个答案:

答案 0 :(得分:14)

  

RAII在多大程度上取代了垃圾收集等其他设计模式?我假设手动内存管理不用于表示系统中的共享所有权

嗯,对于GC,你真的不必关于所有权。只要任何人需要它,对象就会保持不变。共享所有权是默认选择。

当然,所有都可以完成共享所有权。但它有时会导致非常笨拙的代码,因为无法控制或限制对象的生命周期。您必须在finally子句中使用C#的using块或try / finally以及close / dispose调用,以确保在超出范围时清除对象。

在这些情况下,RAII更适合:当对象超出范围时,所有清理都应该自动

RAII在很大程度上取代了GC。 99%的时间,共享所有权并不是您理想的想要的。这是一个可以接受的妥协,以换取通过获取垃圾收集器来节省许多麻烦,但它并不真正匹配您想要的。您希望资源在某个时刻死亡。不是之前,而是之后。当RAII是一个选项时,它会在这些情况下产生更优雅,简洁和健壮的代码。

但是,RAII并不完美。主要是因为它不能很好地处理你只是不知道对象的生命周期的情况。只要有人使用它,它必须保持很长一段时间。但是你不想永远保留它(或者只要围绕所有客户端的范围,这可能只是整个主要功能)。

在这些情况下,C ++用户必须“降级”为共享所有权语义,通常通过shared_ptr引用计数来实现。在那种情况下,GC胜出。它可以更加健壮地实现共享所有权(例如,能够处理周期),并且更有效率(与正常的GC相比,重新计算的摊销成本巨大

理想情况下,我希望用一种语言来看待它们。大多数时候,我想要RAII,但偶尔,我有一种资源,我只想抛到空中而不用担心它何时何地着陆,并且相信它会被清理干净安全。

答案 1 :(得分:8)

程序员的工作就是用他选择的语言优雅地表达事物。

C ++具有非常好的语义,可用于构建和销毁堆栈中的对象。如果可以在范围块的持续时间内分配资源,那么优秀的程序员可能会采用阻力最小的路径。对象的生命周期由大括号分隔,而大括号可能已存在。

如果没有好的方法将对象直接放在堆栈上,也许它可以作为成员放在另一个对象中。现在它的生命周期要长一点,但C ++仍然可以自动运行。对象的生命周期由父对象分隔 - 问题已被委派。

但可能没有一位家长。接下来最好的事情是一系列养父母。这是auto_ptr的用途。仍然相当不错,因为程序员应该知道什么是特定的父母是所有者。对象的生命周期由其所有者序列的生命周期分隔。决定论和本身优雅的一步下来是shared_ptr:由一群业主联盟划定的生命。

但是,此资源可能不与系统中的任何其他对象,对象集或控制流并发。它是在某些事件发生时创建的,并在另一个事件中被破坏。尽管有很多工具可以用来限定代表团和其他生命周期的生命周期,但它们还不足以计算任何任意函数。因此,程序员可能决定编写几个变量的函数来确定对象是否存在或消失,并调用newdelete

最后,编写函数可能很难。也许管理对象的规则会花费太多时间和内存来实际计算!它可能只是非常难以优雅地表达它们,回到我原来的观点。因此,我们有垃圾收集:对象生命周期由您希望的时间和不需要的时间分隔。


对于咆哮很抱歉,但我认为回答问题的最佳方式是上下文:shared_ptr只是计算对象生命周期的工具,适合广泛的替代方案。它工作时有效。它应该在它优雅的时候使用。如果你拥有的所有者少于一个,或者如果你正在尝试使用它作为一种复杂的递增/递减方式来计算某些复杂函数,则不应该使用它。

答案 2 :(得分:4)

  

我的问题:RAII在多大程度上取代了其他设计模式   像垃圾收集?我假设手动内存管理   不用于表示系统中的共享所有权。

我不确定将其称为设计模式,但在同样强烈的意见中,只是谈论内存资源,RAII解决了GC在引入更少内容时可以解决的几乎所有问题。

  

对象的共享所有权是不良设计的标志吗?

我认为共享所有权在大多数案例中远非理想,因为高级别设计并不一定要求它。关于我发现它不可避免的唯一时间是在持久数据结构的实现过程中,它至少内部化为实现细节。

我发现GC的最大问题或者只是共享所有权的一个问题是它在应用程序资源方面没有任何责任的开发者,但可以给出这样做的错觉。如果我们有这样的情况(Scene是资源的唯一逻辑所有者,但其他东西持有一个引用/指针,就像存储用户定义的场景排除列表以省略渲染的相机一样):

enter image description here

让我们说应用程序资源就像一个图像,它的生命周期与用户输入有关(例如:当用户请求关闭包含它的文档时应该释放图像),然后工作到正确释放资源与GC有无相同。

如果没有GC,我们可能会从场景列表中删除它并允许调用其析构函数,同时触发事件以允许Thing1Thing2Thing3设置其指针将它们从列表中删除或删除,以便它们不会有悬空指针。

使用GC,它基本上是一回事。我们从场景列表中删除资源,同时触发事件以允许Thing1Thing2Thing3将其引用设置为null或从列表中删除它们,以便垃圾收集器可以收集它。

在雷达下飞行的沉默程序员错误

这种情况的不同之处在于程序员错误发生时会发生的情况,例如Thing2无法处理删除事件。如果Thing2存储指针,它现在有一个悬空指针,我们可能会崩溃。这是灾难性的,但我们可能很容易在我们的单元和集成测试中捕获,或者至少某些QA或测试人员会很快捕获。我不是在关键任务或安全关键环境中工作,所以如果崩溃的代码设法以某种方式运送,如果我们能够获取错误报告,重现它并检测它仍然不是那么糟糕并快速修复它。

如果Thing2存储了一个强引用并共享所有权,那么我们就会有一个非常无声的逻辑泄漏,并且图片不会被释放,直到Thing2被销毁(它可能不会被销毁)直到关机)。在我的域中,错误的这种沉默本质是非常有问题的,因为即使在发货之后它也可以安静地被忽视,直到用户开始注意到在应用程序中工作一小时导致它占用数十亿字节的内存,例如,并开始放慢速度直到他们重启它。在这一点上,我们可能已经积累了大量的这些问题,因为它很容易像隐形战斗机一样在雷达下飞行,而且除了隐形战斗机之外没有什么比我更不喜欢的了。错误。

enter image description here

由于那种沉默的本性,我倾向于不喜欢与激情分享所有权,TBH我从来不理解为什么GC如此受欢迎(可能是我的特定领域 - 我当然非常不知道任务关键的,例如,我渴望新的语言没有GC 。我发现调查与共享所有权相关的所有此类泄漏非常耗时,有时只需要调查数小时才发现泄漏是由我们无法控制的源代码(第三方插件)引起的。

弱引用

对于Thing1Thing2Thing3来说,弱引用在概念上是理想的。这将允许他们在事后看到资源被销毁的时间而不延长其生命周期,也许我们可以保证在这些情况下崩溃,或者有些人甚至可以在后见之明优雅地处理这个问题。对我来说问题是弱引用可转换为强引用,反之亦然,因此在内部和第三方开发人员之间,有人仍然可能不小心最终在Thing2中存储强引用,即使弱引用也会更合适。

我过去曾尝试过鼓励在内部团队中尽可能多地使用弱引用,并记录它应该在SDK中使用。不幸的是,很难在如此广泛和混杂的群体中推广这种做法,我们仍然最终得到了逻辑泄漏的份额。

任何人,在任何特定时间,通过简单地在对象中存储对它的强引用,可以延长对象的生命周期的时间远远超过一个巨大的代码库,开始变得非常可怕这是在泄漏大量资源。我经常希望有一个非常明确的语法来存储任何类型的强引用作为一种对象的成员,至少会引导开发人员不必要地三思而后行。

明确销毁

所以我倾向于支持对持久性应用程序资源的显式销毁,如下所示:

on_removal_event:
    // This is ideal to me, not trying to release a bunch of strong
    // references and hoping things get implicitly destroyed.
    destroy(app_resource);

...因为我们可以依靠它来释放资源。我们无法完全确定系统中的某些东西最终没有悬挂指针或弱引用,但至少这些问题在测试中往往容易检测和重现。他们不会被忽视并累积。

一个棘手的案例一直是我的多线程。在这些情况下,我发现有用的而不是完整的垃圾收集,或者说,shared_ptr,只是简单地推迟销毁:

on_removal_event:
    // *May* be deferred until threads are finished processing the resource.
    destroy(app_resource);

在某些系统中,持久线程以某种方式统一,使得它们具有processing事件,例如,当线程不存在时,我们可以在时间片中以延迟方式标记要销毁的资源处理过(几乎开始感觉像停止世界的GC,但我们正在保持明确的破坏)。在其他情况下,我们可以使用引用计数,但以避免shared_ptr的方式,其中资源的引用计数从零开始并且将使用上面的显式语法销毁,除非线程本地扩展临时增量计数器的生命周期(例如:在本地线程函数中使用作用域资源)。

就像看似迂回一样,它避免了将GC引用或shared_ptr暴露给外部世界,这很容易诱使某些开发人员(在您的团队或第三方开发人员内部)存储强引用({{1} },例如)作为shared_ptr之类的对象的成员,从而无意中延长了资源的生命周期,并且可能远远超过适当的时间(可能一直到应用程序关闭)。

<强> RAII

同时RAII自动消除物理泄漏以及GC,但此外,它适用于除内存以外的资源。我们可以将它用于作用域互斥体,一个在破坏时自动关闭的文件,我们甚至可以使用它通过范围防护等自动反转外部副作用等。

因此,如果给予选择而我必须选择一个,那对我来说很容易就是RAII。我在一个域中工作,其中由共享所有权引起的那些静默内存泄漏绝对是杀手,并且如果(并且它可能会)在测试期间提前捕获,则悬挂指针崩溃实际上是可取的。即使是在一些非常模糊的事件中,如果它发现很晚,如果它出现在发生错误的网站附近的崩溃中,那么仍然比使用内存分析工具并试图找出谁忘记了在涉及数百万行代码时发布引用。在我非常直率的观点中,GC引入了比我解决的特定域更多的问题(VFX,在场景组织和应用程序状态方面有点类似于游戏),除了那些非常沉默的共享所有权泄漏之外的原因之一是因为它可以给开发人员一种错觉,即他们不必考虑资源管理和持久应用程序资源的所有权,同时无意中造成左右逻辑泄漏。

  

&#34; RAII何时失败&#34;

我在整个职业生涯中遇到过的唯一一个我无法想到避免某种共享所有权的方法的情况是我实现了一个持久数据结构库,如下所示:

enter image description here

我用它来实现一个不可变的网格数据结构,它可以修改部分而不是唯一的,就像这样(用400万个四边形测试):

enter image description here

每一个框架都会创建一个新的网格,因为用户会拖动它并对其进行雕刻。不同之处在于新网格是强引用部分不是刷子独有的部分,因此我们不必复制所有顶点,所有多边形,所有边缘等。不可变版本使线程安全,异常变得微不足道安全,非破坏性编辑,撤消系统,实例化等。

在这种情况下,不可变数据结构的整个概念围绕共享所有权,以避免重复不是唯一的数据。这是一个真实的案例,我们无论如何都无法避免共享所有权(至少我无法想到任何可能的方式)。

这是关于我们可能需要GC或参考计数的唯一情况。其他人可能会遇到他们自己的一些,但根据我的经验,非常非常少的案例真正需要在设计层面共享所有权。

答案 3 :(得分:3)

垃圾收集是一种设计模式吗?我不知道。

共享所有权的一大优势在于其固有的可预测性。使用GC,资源的回收无法完成。这才是重点。何时以及如何发生这种情况通常不是开发人员使用它的想法。通过共享所有权,您可以控制(注意,有时太多控制权是一件坏事)。让我们说你的应用程序会产生一百万个shared_ptr到X.所有这些都是你做的,你负责它们,你完全可以控制这些引用的创建和销毁时间。因此,一个坚定而细致的程序员应该知道参考什么和多长时间的exaclty。如果你想要一个对象被销毁,你必须销毁它的所有共享引用,而中提琴,它已经消失了。

这对制作实时软件的人来说会产生深远的影响,而这些软件必须完全可以预测。这也意味着你可以用看起来很像内存泄漏的方式来捏造。我个人不想成为一个坚定而细心的程序员,当我不必(继续笑,我想继续野餐和骑自行车,不计算我的参考),所以在适当的地方GC是我的首选路线。我编写了一些实时声音软件,并使用共享引用来预测管理资源。

您的问题:RAII什么时候失败? (在共享引用的上下文中) 我的回答:当你无法回答这个问题时:谁可能会提到这个问题?当所有权的恶性无产阶级发展。

我的问题:GC何时失败? 我的回答:当你想要完全控制和可预测性时。当GC是由Sun Microsystems在最后一刻写到截止日期并且具有荒谬的行为时,这些行为只能由从微软借来的严重醉酒的原始人类代码猴子设计和实现。

我的观点:我认为BS对于清晰的设计非常认真。很明显,拥有一个资源被破坏的地方通常比许多可能被破坏的地方更清晰。