代码之家  ›  专栏  ›  技术社区  ›  Khaled Alshaya

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

  •  11
  • Khaled Alshaya  · 技术社区  · 15 年前

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

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

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

    4 回复  |  直到 7 年前
        1
  •  14
  •   Stack Overflow is garbage    13 年前

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

    嗯,有了GC,你不必 认为 关于所有权。物体在周围只要 任何人 需要它。共享所有权是默认的,也是唯一的选择。

    当然,一切 可以 共享所有权。但有时会导致非常笨拙的代码,因为 不能 控制或限制对象的生存期。你必须用C using 块,或 try / finally 在finally子句中使用close/dispose调用,以确保在对象超出范围时将其清除。

    在这些情况下,raii更适合:当对象超出范围时,所有的清理都应该发生。 自动地 .

    RAII在很大程度上取代了GC。99%的时候,共享所有权并不是真正的你 希望 理想的。这是一个可以接受的折衷方案,以换取通过垃圾回收器来节省大量的头痛,但它并不真正符合您的要求。 希望 . 你 希望 资源在某个时刻会消亡。不是之前,也不是之后。当RAII是一个选项时,它会在这些情况下导致更优雅、简洁和健壮的代码。

    不过,雷伊并不完美。主要是因为它不能很好地处理你只是 不知道 对象的生存期。只要有人使用它,它就必须在周围停留很长一段时间。但您不想永远保留它(或者只要围绕所有客户机的范围,这可能只是主要功能的一部分)。

    在这些情况下,C++用户必须“降级”到共享所有权语义,通常通过引用计数实现。 shared_ptr .在这种情况下,GC获胜。它可以更可靠地实现共享所有权(例如,能够处理周期),更有效地(参考计数的摊余成本为 巨大的 与一个像样的GC相比)

    理想情况下,我想看到两种语言。大多数时候,我想要RAII,但偶尔,我有一个资源,我只想把它抛到空中,不担心它什么时候、什么地方降落,只要相信它会在安全的时候被清理干净。

        2
  •  8
  •   Potatoswatter R. Martinho Fernandes    15 年前

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

    C++具有非常好的语义,用于堆栈上对象的构建和销毁。如果一个资源可以在一个作用域块的持续时间内分配,那么一个好的程序员可能会选择阻力最小的路径。对象的生存期由可能已经存在的大括号分隔。

    如果没有好的方法将对象直接放在堆栈上,那么它可能作为成员放在另一个对象中。现在它的生命周期稍长,但C++仍然自动运行。对象的生存期由父对象“限定”,问题已被委托。

    不过,可能没有一个家长。接下来最好的事情是收养父母的顺序。这是什么 auto_ptr 是为了。仍然很好,因为程序员应该知道什么特定的父级是所有者。对象的生存期由其所有者序列的生存期分隔。在决定论和优雅的本质上,一步一步走下去就是 shared_ptr :由所有者池的联合限定的生存期。

    但是,可能这个资源没有与系统中的任何其他对象、对象集或控制流并发。它是在某个事件发生时创建的,在另一个事件发生时销毁。虽然有很多工具可以通过委托和其他生命周期来划分生命周期,但它们不足以计算任意函数。因此,程序员可能决定编写一个由多个变量组成的函数来确定一个对象是存在的还是消失的,然后调用 new delete .

    最后,编写函数可能很困难。也许控制对象的规则需要花费太多的时间和内存来实际计算!也许很难优雅地表达出来,回到我原来的观点。因此,我们有了垃圾收集:对象生存期由您想要它的时间和不想要它的时间分隔。


    很抱歉,我认为回答你问题的最佳方式是上下文: SelddPPTR 只是一个计算物体寿命的工具,它适用于各种各样的选择。它工作的时候就工作。它应该在优雅的时候使用。如果您拥有的所有者少于一个池,或者您正试图计算一些复杂的函数,则不应使用它作为一种复杂的递增/递减方法。

        3
  •  4
  •   Dragon Energy    7 年前
    < Buff行情>

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

    < /块引用>

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

    < Buff行情>

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

    我也有这样的想法,即共享所有权在大多数情况下都是远远不够理想的,因为高级设计并不一定需要它。唯一一次我发现不可避免的是在持久数据结构的实现过程中,它至少被内部化为实现细节。

    我发现gc或只是共享所有权的最大问题是,在应用程序资源方面,它不能免除开发人员的任何责任,但可以给人这样做的假象。如果我们有这样的情况( scene 是该资源的唯一逻辑所有者,但是其他东西持有指向该资源的引用/指针,就像存储由用户定义的场景排除列表的相机,以忽略渲染):。

    假设应用程序资源类似于图像,其生存期与用户输入有关(例如:当用户请求关闭包含该图像的文档时,应释放该图像),那么正确释放该资源的工作与使用或不使用GC时是相同的。

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

    对于GC,基本上是一样的。我们在触发一个事件以允许 thing1>、 thing2 ,and thing3 将它们的引用设置为空或从列表中删除它们以便垃圾收集器可以收集它时,从场景列表中删除资源。

    The silent programmer miss which flying under radar.

    这种情况下的不同之处在于,当发生程序员错误时会发生什么,如 thing2->code>failing to handle the removal event。如果 thing2 stores a pointer,it now have a dangling pointer and we may have a crash.这是灾难性的,但是在我们的单元和集成测试中我们可能很容易捕捉到,或者至少QA或测试人员会很快捕捉到。我不在任务关键型或安全关键型环境中工作,因此,如果死机代码以某种方式成功发布,那么如果我们能够获得错误报告、复制它、检测它并很快修复它,这仍然不是那么糟糕。

    如果 thing2->code>存储了一个强引用并共享所有权,那么我们会有一个非常安静的逻辑泄漏,并且在 thing2->code>被销毁之前,图像不会被释放(关闭之前它可能不会被销毁)。在我的领域中,这种沉默的错误性质是非常有问题的,因为即使在发货之后,它也会悄悄地被忽略,直到用户开始注意到在应用程序中工作一小时会导致它占用千兆字节的内存,例如,在重新启动之前,它会开始减速。在这一点上,我们可能已经积累了大量的这些问题,因为它们很容易像隐形战斗机昆虫一样在雷达下飞行,我最讨厌的就是隐形战斗机昆虫。

    正是由于这种沉默的天性,我倾向于不喜欢热情地分享所有权,而且我从未理解为什么GC如此受欢迎(可能是我的特定领域——我承认我非常不了解那些关键任务的领域,例如),以至于我渴望没有GC的新语言。我发现调查与共享所有权相关的所有此类泄漏非常耗时,有时调查数小时,结果发现泄漏是由我们控制之外的源代码(第三方插件)引起的。

    弱引用

    对于我来说,弱引用在概念上非常适合于 thing1、 thing2和 thing3。这将允许他们在事后发现资源何时被破坏而不延长其生命周期,也许我们可以保证在这种情况下发生崩溃,或者有些人甚至可以在事后优雅地处理这一问题。对我来说,问题在于弱引用可以转换为强引用,反之亦然,因此在现有的内部和第三方开发人员中,仍有人可能粗心大意地将强引用存储在 thing2中 即使弱引用可能更为合适。

    我过去确实尝试过在内部团队中尽可能多地鼓励使用弱引用,并记录应该在SDK中使用它。不幸的是,在这样一个广泛而复杂的群体中推广这种做法是很困难的,我们最终还是分享了我们的逻辑漏洞。

    任何人在任何给定的时间,只要简单地在对象中存储一个对对象的强引用,就可以轻松地将对象的寿命延长得远远超过适当的时间,当我们俯视一个正在泄漏大量资源的巨大代码库时,这种情况开始变得非常可怕。我经常希望需要非常明确的语法来存储任何类型的强引用作为某类对象的成员,这类对象至少会导致开发人员对不必要的操作三思而后行。

    显式销毁

    因此,我倾向于对持久性应用程序资源进行显式破坏,比如:

    关于移除事件: //这对我来说很理想,不想释放一堆强大的 //引用并希望事物被隐式地销毁。 销毁(应用程序资源); < /代码>

    因为我们可以依靠它来释放资源。我们不能完全确信系统中的某些内容最终不会出现悬空指针或弱引用,但至少这些问题在测试中易于检测和重现。它们不会在很长一段时间内被忽视和积累。

    对我来说,最棘手的问题就是多线程。在这些情况下,我发现有用的而不是全面的垃圾收集,或者说, shared&ptr>code>,是简单地延迟销毁以某种方式:

    关于移除事件: //*可以*延迟到线程处理完资源。 销毁(应用程序资源); < /代码>

    在某些系统中,持久线程以某种方式被统一,使它们具有 处理 事件,例如,当线程没有被处理时,我们可以在时间片中以延迟的方式标记要销毁的资源(几乎开始感觉像停止世界GC,但我们保持显式销毁)。在其他情况下,我们可能会使用引用计数,但这样可以避免资源的引用计数从零开始并将使用上面的显式语法销毁,除非线程通过临时增加计数器而在本地延长其生存期(例如:在本地线程函数中使用作用域资源

    正如看起来的那样,它避免了将gc引用或 shared-ptr->code>暴露给外部世界,这很容易吸引一些开发人员(在您的团队或第三方开发人员内部)将强引用( shared-ptr->code>,例如)存储为对象的成员,如 thing2->code>从而扩展资源的LIFETIME不经意间,可能会持续很长时间(可能一直持续到应用程序关闭)。

    raii

    同时,raii自动消除物理泄漏和gc一样,但更进一步地说,它只适用于内存以外的资源。我们可以将它用于一个作用域互斥体,一个在销毁时自动关闭的文件,我们甚至可以使用它通过作用域保护等自动逆转外部副作用。

    所以如果给了我选择,我必须选择一个,这对我来说很容易。我在一个领域工作,在这个领域中,由共享所有权引起的静默内存泄漏绝对是致命的,如果在测试的早期发现悬空指针崩溃(而且很可能会发生),那么悬空指针崩溃实际上是更好的选择。即使是在一些非常模糊的事件中,如果它在发生错误的站点附近发生崩溃,这仍然比使用内存分析工具并试图找出谁在翻阅数百万行代码时忘记释放引用要好。在我直截了当的观点中,GC引入的问题比它为我的特定领域(在场景组织和应用状态方面与游戏有点相似的vfx)解决的问题要多,除了这些非常无声的共享所有权泄漏之外,还有一个原因是它会给开发人员一个错误的印象,即他们不必精简k关于资源管理和持久性应用程序资源的所有权,同时无意中导致左右逻辑泄漏。

    < Buff行情>

    “RAII何时失效”

    < /块引用>

    在我的整个职业生涯中,我遇到过的唯一一个我想不出任何可能的方法来避免某种类型的共享所有权的情况是,当我实现了一个持久数据结构库时,就像这样:

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

    每一帧,一个新的网格正在创建,因为用户拖动它并雕刻它。不同的是,新的网格是强引用部分,不是由画笔使唯一的,这样我们就不必复制所有的顶点,所有的多边形,所有的边缘等。不变的版本琐碎了线程安全,例外安全,非破坏性编辑,撤销系统,实例等。

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

    这是我们遇到的唯一可能需要GC或引用计数的情况。其他人可能也遇到过自己的问题,但根据我的经验,很少有情况真正需要在设计级别上共享所有权。

    ment 不用于表示系统中的共享所有权。

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

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

    我有一种想法,即共享所有权在 案例,因为高级设计不一定需要它。唯一一次我发现不可避免的是在持久数据结构的实现过程中,它至少被内部化为实现细节。

    我发现gc或只是共享所有权的最大问题是,在应用程序资源方面,它不能免除开发人员的任何责任,但可以给人这样做的假象。如果我们有这样的案子( Scene 是该资源的唯一逻辑所有者,但其他内容会保存指向该资源的引用/指针,就像相机存储用户定义的场景排除列表以忽略渲染一样):

    enter image description here

    假设应用程序资源类似于一个映像,其生存期与用户输入有关(例如:当用户请求关闭包含该映像的文档时,应该释放该映像),那么正确释放该资源的工作与使用或不使用GC是相同的。

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

    对于GC,基本上是一样的。我们从场景列表中删除资源,同时触发一个事件以允许 TIGN1 , TIGN2 三号 将它们的引用设置为空或从列表中移除它们,以便垃圾收集器可以收集它。

    在雷达下飞行的无声程序错误

    在这种情况下,不同之处在于当程序员出错时会发生什么,比如 TIGN2 无法处理删除事件。如果 TIGN2 存储一个指针,它现在有一个悬空指针,我们可能会崩溃。这是灾难性的,但是在我们的单元和集成测试中我们可能很容易捕捉到,或者至少QA或测试人员会很快捕捉到。我不在任务关键或安全关键的环境中工作,所以如果崩溃的代码以某种方式成功地发布,那么如果我们能够得到一个bug报告,复制它,检测它并很快地修复它,那就没有那么糟糕了。

    如果 TIGN2 存储一个强大的引用并共享所有权,我们有一个非常安静的逻辑泄漏,并且在 TIGN2 被销毁(在关闭之前可能不会销毁)。在我的领域中,这种沉默的错误性质是非常有问题的,因为即使在发货之后,它也会悄悄地被忽略,直到用户开始注意到在应用程序中工作一小时会导致它占用千兆字节的内存,例如,在重新启动之前,它会开始减速。在那一点上,我们可能已经积累了大量的这些问题,因为它们很容易像隐形战斗机一样在雷达下飞行,我最讨厌的就是隐形战斗机。

    enter image description here

    正是因为这种沉默的天性,我才不喜欢共享的所有权,而且我也不明白为什么GC如此受欢迎(可能是我的特定领域——诚然,我对那些关键任务的领域非常无知,例如),以至于我渴望新的语言。 无气相色谱法 . 我发现调查与共享所有权相关的所有此类泄漏非常耗时,有时调查数小时,结果发现泄漏是由我们控制之外的源代码(第三方插件)造成的。

    弱引用

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

    我过去确实尝试过在内部团队中尽可能多地鼓励使用弱引用,并记录应该在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,但我们保留了显式销毁)。在其他情况下,我们可以使用引用计数,但要避免 SelddPPTR ,其中资源的引用计数从零开始,并将使用上面的显式语法销毁,除非线程通过临时增加计数器在本地延长其生存期(例如:在本地线程函数中使用作用域资源)。

    正如看起来的那样,它避免公开GC引用或 共享资源 到外部世界,这很容易吸引一些开发人员(在您的团队或第三方开发人员内部)存储强引用( SelddPPTR 例如)作为对象的成员 TIGN2 从而不经意地延长资源的生命周期,可能会比适当的时间长得多(可能一直到应用程序关闭)。

    拉伊

    同时,raii自动消除物理泄漏和gc一样,但更进一步地说,它只适用于内存以外的资源。我们可以将它用于一个作用域互斥体,一个在销毁时自动关闭的文件,我们甚至可以使用它通过作用域保护等自动逆转外部副作用。

    所以如果给了我选择,我必须选择一个,这对我来说很容易。我在一个领域工作,在这个领域中,由共享所有权引起的静默内存泄漏绝对是致命的,如果在测试的早期发现悬空指针崩溃(而且很可能会发生),那么悬空指针崩溃实际上是更好的选择。即使是在一些非常模糊的事件中,如果它在发生错误的站点附近发生崩溃,这仍然比使用内存分析工具并试图找出谁在翻阅数百万行代码时忘记释放引用要好。在我直截了当的观点中,GC引入的问题比它为我的特定领域(在场景组织和应用状态方面与游戏有点相似的vfx)解决的问题要多,除了这些非常无声的共享所有权泄漏之外,还有一个原因是它会给开发人员一个错误的印象,即他们不必精简k关于资源管理和持久性应用程序资源的所有权,同时无意中导致左右逻辑泄漏。

    “RAII何时失效”

    在我的整个职业生涯中,我遇到过的唯一一个我想不出任何可能的方法来避免某种类型的共享所有权的情况是,当我实现了一个持久数据结构库时,就像这样:

    enter image description here

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

    enter image description here

    每一帧,一个新的网格正在创建,因为用户拖动它并雕刻它。不同的是,新的网格是强引用部分,不是由画笔使唯一的,所以我们不必复制所有的顶点,所有的多边形,所有的边缘等。不变的版本琐碎了线程安全,例外安全,非破坏性编辑,撤消系统,实例等。

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

    这是我们遇到的唯一可能需要GC或引用计数的情况。其他人可能也遇到过自己的问题,但根据我的经验,很少有情况真正需要在设计级别上共享所有权。

        4
  •  3
  •   Mankarse    12 年前

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

    共享所有权的最大优势在于其固有的可预测性。有了GC,资源的回收就不在你手上了。这就是重点。使用它的开发人员通常不会考虑它的时间和方式。拥有共同的所有权,你就可以控制(小心,有时控制太多是件坏事)。假设你的应用程序产生了一百万个共享到X的ptr。所有这些都是你做的,你要对它们负责,你完全可以控制何时创建和销毁这些引用。所以一个有决心和细心的程序员应该知道谁引用了什么以及引用了多长时间。如果你想销毁一个对象,你必须销毁所有对它的共享引用,维奥拉,它就不见了。

    这给制作实时软件的人带来了深刻的影响,而这些软件必须是完全可预测的。这也意味着你可以用看起来很像内存泄漏的方式来敷衍自己。我个人不想成为一个坚定和谨慎的程序员当我不必(去笑吧,我想去野餐和骑自行车,不算我的参考),所以在适当的地方GC是我的首选路线。我编写了一些实时声音软件,并使用共享的参考资料来管理可预测的资源。

    你的问题是:RAII什么时候会失败?(在共享引用的上下文中) 我的回答是:当你不能回答这个问题的时候:谁可以参考这个?当恶性的、平淡的所有权循环发展时。

    我的问题:GC什么时候失败? 我的回答是:当你想要完全控制和可预测性的时候。当GC是由Sun Microsystems在最后一分钟内完成的,并且具有荒谬的行为时,这些行为只能由从微软借来的酗酒的原始人类代码猴子来设计和实现。

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