11

Java and .NET have wonderful garbage collectors that manage memory for you, and convenient patterns for quickly releasing external objects (Closeable, IDisposable), but only if they are owned by a single object. In some systems a resource might need to be consumed independently by two components, and only be released when both components release the resource.

In modern C++ you would solve this problem with a shared_ptr, which would deterministically release the resource when all the shared_ptr's are destroyed.

Are there any documented, proven patterns for managing and releasing expensive resources that don't have a single owner in object oriented, non-deterministically garbage collected systems?

3
  • 1
    Have you seen Clang's Automatic Reference Counting, also used in Swift?
    – jscs
    Commented Dec 12, 2017 at 13:56
  • 1
    @JoshCaswell Yes, and that would solve the problem, but I'm working in a garbage collected space.
    – C. Ross
    Commented Dec 12, 2017 at 13:59
  • 8
    Reference Counting is a Garbage Collection strategy. Commented Dec 12, 2017 at 14:01

5 Answers 5

15

In general, you avoid it by having a single owner - even in unmanaged languages.

But the principle is the same for managed languages. Instead of immediately closing the expensive resource on a Close() you decrement a counter (incremented on Open()/Connect()/etc) until you hit 0 at which point the close actually does the close. It'll likely look and act like the Flyweight Pattern.

7
  • This is what I was thinking as well, but is there a documented pattern for it? Flyweight is certainly similar, but specifically for memory as its usually defined.
    – C. Ross
    Commented Dec 12, 2017 at 14:00
  • @C.Ross This seems to be a case in which finalizers are encouraged. You can use a wrapper class around the unmanaged resource, adding a finalizer to that class to release the resource. You can also have it implement IDisposable, keep counts to release the resource as soon as possible, etc. Probably the best thing, a lot of times, is to have all three, but the finalizer is probably the most critical part, and the IDisposable implementation is the least critical. Commented Dec 12, 2017 at 14:05
  • 11
    @Panzercrisis except that finalizers are not guaranteed to run, and especially not guaranteed to run promptly.
    – Caleth
    Commented Dec 12, 2017 at 14:17
  • @Caleth I was thinking the counts thing would help with the promptness part. As far as them not running at all goes, do you mean the CLR might just not get around to it before the program ends, or do you mean they might get disqualified outright? Commented Dec 12, 2017 at 14:28
  • 19
14

In a garbage collected language (where GC is not deterministic), it is not possible to reliably tie the cleanup of a resource other than memory to the lifetime of an object: It is not possible to state when an object will be deleted. The end of the lifetime is entirely at the discretion of the garbage collector. The GC only guarantees that an object will live while it is reachable. Once an object becomes unreachable it may be cleaned up at some point in the future, which may involve running finalizers.

The concept of “resource ownership“ doesn't really apply in a GC language. The GC system owns all objects.

What these languages do offer with try-with-resource + Closeable (Java), using statements + IDisposable (C#), or with statements + context managers (Python) is a way for control flow (!= objects) to hold a resource that is closed when the control flow leaves a scope. In all of these cases, this is similar to an automatically inserted try { ... } finally { resource.close(); }. The lifetime of the object representing the resource is not related to the lifetime of the resource: the object may continue to live after the resource was closed, and the object may become unreachable while the resource is still open.

In the case of local variables, these approaches are equivalent to RAII, but need to be used explicitly at the call site (unlike C++ destructors which will run by default). A good IDE will warn when this is omitted.

This does not work for objects that are referenced from locations other than local variables. Here, it's irrelevant whether there are one or more references. It is possible to translate resource referencing via object references to resource ownership via control flow by creating a separate thread that holds this resource, but threads too are resources that need to be discarded manually.

In some cases it is possible to delegate resource ownership to a calling function. Instead of temporary objects referencing resources that they should (but cannot) clean up reliably, the calling function holds a set of resources that need to be cleaned up. This only works until the lifetime of any of these objects outlives the lifetime of the function, and therefore references a resource that has already been closed. This cannot be detected by a compiler, unless the language has Rust-like ownership tracking (in which case there are already better solutions for this resource management problem).

This leaves as the only viable solution: manual resource management, possibly by implementing reference counting yourself. This is error-prone, but not impossible. In particular, having to think about ownership is unusual in GC languages, so existing code may not be sufficiently explicit about ownership guarantees.

3

Lots of good information from the other answers.

Still, to be explicit, the pattern you might be looking for is that you use small singly-owned objects for the RAII-like control flow construct via using and IDispose, in conjunction with a (larger, possibly reference counted) object that holds some (operating system) resources.

So there's the small unshared single owner objects that (via the smaller object's IDispose and the using control flow construct) can in turn inform the larger shared object (perhaps custom Acquire & Release methods).

(The Acquire and Release methods shown below are then also available outside of the using construct, but without the safety of the try implicit in using.)


An example in 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;
    }
}
2
  • If that should be C# (which it looks like) then your Reference<T> implementation is subtly incorrect. The contract for IDisposable.Dispose states that calling Dispose multiple times on the same object must be a no-op. If I were to implement such a pattern I'd also make Release private to avoid unnecessary errors and use delegation instead of inheritance (remove the interface, provide a simple SharedDisposable class that can be used with arbitrary Disposables), but those are more matters of taste.
    – Voo
    Commented Dec 12, 2017 at 23:46
  • @Voo, ok, good point, thx!
    – Erik Eidt
    Commented Dec 13, 2017 at 1:15
1

The vast majority of objects in a system should generally fit one of three patterns:

  1. Objects whose state will never change, and to which references are held purely as a means of encapsulating the state. Entities that hold references neither know nor care about whether any other entities hold references to the same object.

  2. Objects which are under the exclusive control of a single entity, which is the sole owner of all state therein, and uses the object purely as a means of encapsulating the (possibly mutable) state therein.

  3. Objects which are owned by a single entity, but which other entities are allowed to use in limited ways. The owner of the object may use it not only as a means of encapsulating state, but also encapsulating a relationship with the other entities that share it.

Tracking garbage-collection works better than reference counting for #1, because code that uses such objects doesn't need to do anything special when it's done with the last remaining reference. Reference-counting isn't needed for #2 because objects will have exactly one owner, and it will know when it no longer needs the object. Scenario #3 may pose some difficulty if the owner of an object kills it while other entities still hold references; even there, a tracking GC may be better than reference counting at ensuring that references to dead objects remain reliably identifiable as references to dead objects, for as long as any such references exist.

There are a few situations where it may be necessary to have an shareable owner-less object acquire and hold external resources as long as anyone needs its services, and should release them when its services are no longer required. For example, an object which encapsulates the contents of a read-only file could be shared and used by many entities simultaneously without any of them having to know or care about each other's existence. Such circumstances are rare, however. Most objects will either have a single clear owner, or else be owner-less. Multiple ownership is possible, but seldom useful.

0

Shared Ownership Rarely Makes Sense

This answer might be slightly off-tangent, but I have to ask, how many cases does it make sense from a user-end standpoint to share ownership? At least in the domains I've worked in, there were practically none because otherwise that would imply that the user doesn't need to simply remove something one time from one place, but explicitly remove it from all relevant owners before the resource is actually removed from the system.

It's often a lower-level engineering idea to prevent resources from being destroyed while something else is still accessing it, like another thread. Often when a user requests to close/remove/delete something from the software, it should be removed as soon as possible (whenever it is safe to remove), and it certainly shouldn't linger around and cause a resource leak for as long as the application is running.

As an example, a game asset in a video game might reference a material from the material library. We certainly don't want, say, a dangling pointer crash if the material is removed from the material library in one thread while another thread is still accessing the material referenced by the game asset. But that doesn't mean it makes any sense for game assets to share ownership of materials they reference with the material library. We don't want to force the user to explicitly remove the material from both asset and material library. We just want to make sure that materials are not removed from the materiel library, the sole sensible owner of materials, until other threads are finished accessing the material.

Resource Leaks

Yet I worked with a former team that embraced GC for all components in the software. And while that really helped in making sure we never had resources being destroyed while other threads were still accessing them, we instead ended up getting our share of resource leaks.

And these were not trivial resource leaks of a kind that upsets only developers, like a kilobyte of memory leaked after an hour-long session. These were epic leaks, often gigabytes of memory over an active session, leading to bug reports. Because now when a resource's ownership is being referenced (and therefore shared in ownership) among, say, 8 different parts of the system, then it only takes one to fail to remove the resource in response to the user requesting it to be removed for it to be leaked and possibly indefinitely.

So I was never a huge fan of GC or reference counting applied at any wide scale because of how easy they made it to create leaky software. What would have formerly been a dangling pointer crash which is easy to detect turns into a very difficult-to-detect resource leak which can easily fly under the radar of testing.

Weak references can mitigate this issue if the language/library provides these, but I found it difficult to get a team of developers of mixed skillsets to be able to consistently use weak references whenever appropriate. And this difficulty wasn't just related to the internal team, but to every single plugin developer for our software. They too could easily cause the system to leak resources by just storing a persistent reference to an object in ways that made it difficult to trace back to the plugin as the culprit, so we also got our lion's share of bug reports resulting from our software resources being leaked simply because a plugin whose source code was outside of our control failed to release references to those expensive resources.

Solution: Deferred, Periodic Removal

So my solution later on which I applied to my personal projects that gave me kind of the best I found from both worlds was to eliminate the concept that referencing=ownership but still have deferred destruction of resources.

As a result, now whenever the user does something that causes a resource to need removal, the API is expressed in terms of just removing the resource:

ecs->remove(component);

... which models the user-end logic in a very straightforward way. However, the resource (component) may not be removed right away if there are other system threads in their processing phase where they could be accessing the same component concurrently.

So these processing threads then yield time here and there which allows a thread which resembles a garbage collector to wake up and "stop the world" and destroy all resources which were requested to be removed while locking out threads from processing those components until it is finished. I've tuned this so that the amount of work needing to be done here is generally minimal and doesn't cut noticeably into frame rates.

Now I can't say this is some tried and tested and well-documented method, but it's something I've been using for a few years now with no headaches whatsoever and no resource leaks. I recommend exploring approaches like this when it's possible for your architecture to fit this kind of concurrency model as it's a lot less heavy-handed than GC or ref-counting and doesn't risk these types of resource leaks flying under the radar of testing.

The one place where I found ref-counting or GC to be useful is for persistent data structures. In that case it's data structure territory, far divorced from user-end concerns, and there it does actually make sense for each immutable copy to potentially be sharing ownership of the same unmodified data.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.