内存管理语言的参考计数模式?


11

Java和.NET都为你管理内存美妙的垃圾收集器,方便的方式快速释放的外部对象(CloseableIDisposable),但只有当他们是由一个单一的对象所拥有。在某些系统中,可能需要由两个组件独立消耗资源,并且仅在两个组件都释放资源时才释放资源。

在现代C ++中,您可以使用来解决此问题,shared_ptr当所有shared_ptr都被销毁时,它将确定性地释放资源。

在面向对象的,不确定的垃圾收集系统中,是否有任何记录的,经过验证的模式来管理和释放昂贵的资源,这些资源没有一个所有者?


1
您是否看过也在Swift中使用过的Clang的自动引用计数
jscs

1
@JoshCaswell是的,这可以解决问题,但是我正在垃圾收集空间中工作。
C. Ross

8
参考计数垃圾收集策略。
约尔格W¯¯米塔格

Answers:


15

通常,您可以通过拥有一个所有者来避免这种情况-即使使用非托管语言也是如此。

但是对于托管语言,原理是相同的。而不是立即关闭Close()计数器上的昂贵资源,而是递减计数器(在Open()/ Connect()/ etc 上递增),直到您达到0为止,此时关闭实际上就是关闭。它的外观和行为可能像“轻量级模式”一样。


这也是我的想法,但是是否有记录的模式?Flyweight当然是相似的,但通常是针对其通常定义的内存。
C. Ross

@ C.Ross这似乎是鼓励使用终结器的情况。您可以在非托管资源周围使用包装器类,将终结器添加到该类中以释放资源。您也可以使用它来实现IDisposable,保持计数以尽快释放资源,等等。很多时候最好的办法是同时拥有这三个,但是终结器可能是最关键的部分,IDisposable实现是最不重要的。
Panzercrisis

11
@Panzercrisis,除了不能保证finalizers可以运行,尤其是不能保证它们能迅速运行。
卡雷斯(Caleth)'17

@Caleth我在想计数工作将有助于快速处理部分。就它们根本不运行而言,您是说CLR可能只是在程序结束前就不了解它,还是意味着它们可能会完全失去资格?
Panzercrisis


14

在垃圾回收语言(GC不是确定性的)中,无法可靠地将除内存以外的资源清理与对象的生存期联系起来:无法声明何时删除对象。生命周期的结束完全由垃圾收集器决定。GC仅保证对象可访问时将生存。一旦某个对象变得不可访问,它可能会在将来的某个时间清理,这可能涉及运行终结器。

“资源所有权”的概念实际上并不适用于GC语言。GC系统拥有所有对象。

这些语言在try-with-resource + Closeable(Java),语句+ IDisposable(C#)或语句+上下文管理器(Python)的作用下,是控制流(!=对象)持有资源的一种方式当控制流离开合并范围时关闭。在所有这些情况下,这类似于自动插入try { ... } finally { resource.close(); }。表示资源的对象的生存期与资源的生存期无关:对象在关闭资源后可能会继续存在,并且在资源仍处于打开状态时对象可能变得无法访问。

对于局部变量,这些方法等效于RAII,但需要在调用站点上显式使用(与默认运行的C ++析构函数不同)。一个好的IDE会在省略时发出警告。

这不适用于从局部变量以外的位置引用的对象。在这里,是否有一个或多个引用无关紧要。通过创建保存该资源的单独线程,可以将通过对象引用的资源引用转换为通过控制流的资源所有权,但是线程也是需要手动丢弃的资源。

在某些情况下,可以将资源所有权委派给调用函数。调用函数拥有一组需要清除的资源,而不是临时对象引用应可靠(但不能)清除的资源。这仅在这些对象中的任何一个对象的生存期超过函数的生存期,并因此引用已关闭的资源之前有效。除非该语言具有类似Rust的所有权跟踪功能,否则编译器无法检测到这种情况(在这种情况下,已经有更好的解决方案来解决此资源管理问题)。

剩下的唯一可行的解​​决方案是:手动资源管理,可能通过实现引用计数来实现。这容易出错,但并非不可能。特别是,在GC语言中,必须考虑所有权是不常见的,因此现有代码可能不足以明确表明所有权保证。


3

其他答案提供了很多有用的信息。

仍然要明确地说,您可能正在寻找的模式是,通过using和使用一个单独的小型对象用于类似RAII的控制流构造,以及IDispose一个容纳一些(操作对象)的(较大的,可能是引用计数的)对象系统)资源。

因此,有一些小的未共享的单一所有者对象(通过较小的对象IDisposeusing控制流构造)可以依次通知较大的共享对象(也许是custom AcquireReleasemethod)。

(下面显示的AcquireRelease方法也可以在using构造之外使用,但是没有try隐式安全的安全性using。)


C#中的一个例子

void Test ( MyRefCountedClass myObj )
{
    using ( var usingRef = myObj.Acquire () )
    {
        var item = usingRef.Item;
        item.SomeMethod ();

        // the `using` automatically invokes Dispose() on usingRef
        //  which in turn invokes Release() on `myObj.
    }
}

interface IReferencable<T> where T: IReferencable<T> {
    Reference<T> Acquire ();
    void Release();
}

struct Reference<T>: IDisposable where T: IReferencable<T>
{
    public readonly T Item;
    public Reference(T item) { Item = item; _released = false; }
    public void Dispose() { if (! _released ) { _released = true; Item.Release(); } }
    private bool _released;
}

class MyRefCountedClass : IReferencable<MyRefCountedClass>
{
    private int _refCount = 0;

    public Reference<MyRefCountedClass> Acquire ()
    {
        _refCount++;
        return new Reference<MyRefCountedClass>(this);
    }

    public void Release ()
    {
        if (--_refCount <= 0)
            Dispose();
    }

    // NOTE that MyRefCountedClass does not have to implement IDisposable, but it can...
    // as shown here it doesn't implement the interface
    private void Dispose ()  
    {
        if ( _refCount > 0 )
            throw new Exception ("Dispose attempted on item in use.");
        // release other resources...
    }

    public int SomeMethod()
    {
        return 0;
    }
}

如果那应该是C#(看起来像这样),那么您的Reference <T>实现确实是不正确的。对于在同一对象上多次IDisposable.Dispose调用Dispose必须为空操作的州的合同。如果要实现这种模式,我也将Release私有化以避免不必要的错误,并使用委托而不是继承(删除接口,提供一个SharedDisposable可以与任意Disposables一起使用的简单类),但是这更多的是趣味性。
Voo

@Voo,好的,很好,谢谢!
艾瑞克·艾德

1

系统中的绝大多数对象通常应符合以下三种模式之一:

  1. 状态永远不会改变的对象,对其引用的保留纯粹是封装状态的一种手段。持有引用的实体既不知道也不关心其他实体是否持有对同一对象的引用。

  2. 在单个实体的排他控制下的对象,该实体是其中所有状态的唯一所有者,并且仅将对象用作在其中封装(可能可变的)状态的手段。

  3. 由单个实体拥有,但允许其他实体以有限方式使用的对象。对象的所有者不仅可以将其用作封装状态的手段,而且还可以封装与共享该对象的其他实体的关系。

跟踪垃圾收集的效果比#1的引用计数更好,因为使用此类对象的代码在处理完最后一个引用后不需要做任何特殊的事情。#2不需要引用计数,因为对象将只有一个所有者,并且它将知道何时不再需要该对象。如果对象的所有者在其他实体仍然持有引用的情况下杀死了对象,则方案3可能会带来一些困难。即使在那儿,只要存在任何此类引用,跟踪GC也可能比引用计数更好,以确保对死对象的引用可以可靠地标识为对死对象的引用。

在某些情况下,只要有人需要它的服务,就可能有必要让一个可共享的无所有者对象获取并保存外部资源,并在不再需要其服务时释放它们。例如,封装了只读文件内容的对象可以由许多实体同时共享和使用,而无需任何实体彼此了解或关心彼此的存在。但是,这种情况很少见。大多数对象要么只有一个明确的所有者,要么是没有所有者的。多重所有权是可能的,但很少有用。


0

共享所有权几乎没有道理

这个答案可能有点不切实际,但是我不得不问,从用户端的角度来看,共享所有权有多少种情况有意义?至少在我工作过的领域中,几乎没有,因为这意味着用户不需要一次从一个地方删除某件事,而是在资源真正使用之前将其从所有相关所有者中删除。从系统中删除。

防止其他人仍在访问资源(例如另一个线程)时破坏资源通常是一个较低层的工程思想。通常,当用户要求关闭/删除/删除软件中的某些内容时,应尽快将其删除(只要可以安全删除),并且它肯定不会流连忘返并导致资源泄漏该应用程序正在运行。

例如,视频游戏中的游戏资产可能引用了素材库中的素材。例如,我们肯定不希望悬空的指针崩溃,如果在一个线程中从材料库中删除了该材料,而另一个线程仍在访问游戏资产所引用的材料时。但这并不意味着游戏资产与素材库共享它们引用的素材所有权。我们不想强迫用户从资产和物料库中显式删除物料。我们只想确保在其他线程完成对材料的访问之前,不要将材料从唯一的材料所有者材料库中删除。

资源泄漏

但是我曾与一个以前的团队合作过,该团队在软件中的所有组件都使用了GC。虽然这确实有助于确保在其他线程仍在访问它们时我们绝不会销毁资源,但最终却导致了资源泄漏

而且这些也不是琐碎的资源泄漏,这种泄漏只会使开发人员感到不适,就像在一个小时的会话后泄漏了千字节的内存一样。这些都是史诗性的泄漏,通常在活动会话中存在数GB的内存,从而导致错误报告。因为现在,当在例如系统的8个不同部分之间引用资源所有权(并因此在所有权中共享)时,响应于用户请求将其删除,仅需一个失败即可删除资源被泄漏,并可能无限期地泄漏。

因此,我从来都不是GC或大规模应用引用计数的忠实拥护者,因为他们使创建泄漏软件变得如此容易。以前是悬而未决的指针崩溃,它很容易检测到,变成了很难检测的资源泄漏,很容易在测试的雷达下飞行。

如果语言/库提供了弱引用,则弱引用可以缓解此问题,但是我发现很难让一组混合技能组的开发人员能够在适当的情况下始终使用弱引用。这个困难不仅与内部团队有关,还与我们软件的每个插件开发人员有关。它们也很容易通过仅存储对对象的持久引用而以某种方式导致难以追溯到作为罪魁祸首的插件,从而很容易导致系统泄漏资源,因此我们也从我们的软件资源中获得了大部分错误报告。仅仅因为源代码不在我们控制范围之内的插件无法释放对那些昂贵资源的引用而被泄漏。

解决方案:延迟定期删除

因此,后来我将其应用于我的个人项目的解决方案使我从两个方面都得到了最好的帮助,那就是消除referencing=ownership仍然延迟资源破坏的概念。

结果,现在,只要用户执行导致需要删除资源的操作,API就会以删除资源的方式表示:

ecs->remove(component);

...以非常直接的方式对用户端逻辑进行建模。但是,如果在处理阶段有其他系统线程可以同时访问同一组件,则可能不会立即删除资源(组件)。

因此,这些处理线程然后在这里到处产生时间,这使得类似于垃圾收集器的线程可以唤醒并“ 停止运行 ”并销毁所有需要删除的资源,同时将线程锁定在处理这些组件之前,直到完成。我已对此进行了调整,以使此处需要完成的工作量通常最少,并且不会明显降低帧速率。

现在,我不能说这是一种经过尝试和测试且有据可查的方法,但这是我已经使用了几年的东西,没有任何麻烦,也没有资源泄漏。我建议您在架构可能适合这种并发模型的情况下探索这种方法,因为它比GC或ref计数要费力得多,并且不会冒着在测试范围内泄漏这些类型的资源泄漏的风险。

我发现引用计数或GC有用的一个地方是持久数据结构。在那种情况下,它是数据结构的领域,与用户端的关注点相去甚远,对于每个不可变的副本来说,共享相同的未修改数据的所有权实际上是有意义的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.