Jump to page: 1 24  
Page
Thread overview
GC-proof resource classes
Aug 29, 2015
ponce
Aug 29, 2015
cym13
Aug 29, 2015
ponce
Aug 29, 2015
cym13
Aug 29, 2015
ponce
Aug 29, 2015
cym13
Aug 29, 2015
rsw0x
Aug 29, 2015
cym13
Aug 29, 2015
Timon Gehr
Aug 29, 2015
rsw0x
Aug 29, 2015
Timon Gehr
Aug 30, 2015
rsw0x
Aug 30, 2015
Timon Gehr
Aug 29, 2015
cym13
Aug 29, 2015
Timon Gehr
Aug 30, 2015
cym13
Aug 30, 2015
Jim Hewes
Aug 30, 2015
rsw0x
Aug 29, 2015
ponce
Aug 29, 2015
skoppe
Aug 30, 2015
ponce
Aug 30, 2015
skoppe
Aug 30, 2015
ponce
Aug 30, 2015
ZombineDev
Aug 30, 2015
ponce
Aug 30, 2015
ZombineDev
Aug 30, 2015
ponce
Aug 30, 2015
Brian Schott
Aug 31, 2015
ponce
Aug 31, 2015
Sebastiaan Koppe
Aug 31, 2015
ponce
Aug 31, 2015
byron
Aug 31, 2015
ponce
Sep 01, 2015
cym13
Sep 01, 2015
ponce
August 29, 2015
As a library writer I've struggled a bit with to provide easy resource clean-up despite using class objects.

Is there reasons to use class objects that hold resources in the first place vs Unique!T or RefCounted!T structs?
I think yes:
- classes have reference semantics without naming or runtime overhead: you read Resource and not RefCounted!Resource
- no need to disable the postblit or have a valid .init is another thing
That's about it from the top of my head and it may not be good reasons!

As you probably know GC-called destructors enjoy a variety of limitations:
- can't allocate,
- can't access members,
- aren't guaranteed to happen at all.

This makes GC-called destructors mostly useless for non-memory resource release. IIRC Andrei suggested once that destructors shouldn't be called at all by the GC, something that I agree with.

As such, some of us started providing a release()/dispose()/close() method, and have the destructor call it to support both scoped ownership and manual release.

But that introduce accidentally correct design when the destructor is called by GC, and avoids a leak. This is arguably worse than the initial problem.

It turns out separating calls of ~this() that are called by the GC from those that are called for a deterministic reason is enough, and support all cases I can think of: Unique!T/scoped!T/.destroy/RefCounted!T/manual/GC-called

It works like this:

----------------

class MyResource
{
    void* handle;

    this()
    {
        handle = create_handle();
    }

    ~this()
    {
        if (handle != null) // must support repeated calls for the case (called by .destroy + called by GC later)
        {
            ensureNotInGC("MyResource");
            free_handle(handle);
        }
    }
}

----------------

ensureNotInGC() is implemented like this:

----------------

void ensureNotInGC(string resourceName) nothrow
{
    debug
    {
        import core.exception;
        try
        {
            import core.memory;
            void* p = GC.malloc(1); // not ideal since it allocates
            return;
        }
        catch(InvalidMemoryOperationError e)
        {
            import core.stdc.stdio;
            fprintf(stderr, "Error: clean-up of %s incorrectly depends on destructors called by the GC.\n", resourceName.ptr);
            assert(false); // crash
        }
    }
}

--------------

Looks ugly? Yes, but it makes the GC acts as a cheap leak detector, giving accurate messages for still opened resources.


August 29, 2015
On Saturday, 29 August 2015 at 13:14:26 UTC, ponce wrote:
> This makes GC-called destructors mostly useless for non-memory resource release. IIRC Andrei suggested once that destructors shouldn't be called at all by the GC, something that I agree with.
>
> As such, some of us started providing a release()/dispose()/close() method, and have the destructor call it to support both scoped ownership and manual release.

I'm not sure it is the best way to do things... In Python for example we have a GC that calls the default destructor (__del__ method). As a consequence, if you need some resource to be freed you have to do it explicitely by writting a close()/whatever() method. But nobody's linking the destructor to it because of the separation of concerns principle: we release what has to be released and only that: freeing the object is the realm of the GC. This mixed style is something I have yet to encounter in D where it could be way more powerful than in Python: free what you must, not what you can.

> But that introduce accidentally correct design when the destructor is called by GC, and avoids a leak. This is arguably worse than the initial problem.

I'd like to see a concrete example of this, it seems I'm missing something...

> void ensureNotInGC(string resourceName) nothrow
> {
>     debug
>     {
>         import core.exception;
>         try
>         {
>             import core.memory;
>             void* p = GC.malloc(1); // not ideal since it allocates
>             return;
>         }
>         catch(InvalidMemoryOperationError e)
>         {
>             import core.stdc.stdio;
>             fprintf(stderr, "Error: clean-up of %s incorrectly depends on destructors called by the GC.\n", resourceName.ptr);
>             assert(false); // crash
>         }
>     }
> }
>
> --------------
>
> Looks ugly? Yes, but it makes the GC acts as a cheap leak detector, giving accurate messages for still opened resources.

As I said before, I'm not sure preventing the GC from doing its collection job is a good idea, but I find the concept of having such a leak detector really interesting!

August 29, 2015
On Saturday, 29 August 2015 at 13:43:33 UTC, cym13 wrote:
>> But that introduce accidentally correct design when the destructor is called by GC, and avoids a leak. This is arguably worse than the initial problem.
>
> I'd like to see a concrete example of this, it seems I'm missing something...
>

Example 1:

You forget to release Resource A. The GC happen to call A destructor that releases it. But GC destructors are not guaranteed to happen.
See http://dlang.org/class.html ("The garbage collector is not guaranteed to run the destructor for all unreferenced objects").


Example 2:

Resource A depends on Resource B. You forget to release either. The GC happens to call A and B destructors in the right order, by chance. A new D release changes this order later.





August 29, 2015
On Saturday, 29 August 2015 at 13:43:33 UTC, cym13 wrote:
> But nobody's linking the destructor to it because of the separation of concerns principle: we release what has to be released and only that: freeing the object is the realm of the GC.

I see what you mean, but Unique!T, RefCounted!T and scoped!T call the destructor, not the release() function you just defined. So separating concerns break those.
August 29, 2015
On Saturday, 29 August 2015 at 13:58:07 UTC, ponce wrote:
> Example 1:
>
> You forget to release Resource A. The GC happen to call A destructor that releases it. But GC destructors are not guaranteed to happen.
> See http://dlang.org/class.html ("The garbage collector is not guaranteed to run the destructor for all unreferenced objects").

This, ok, it is the common GC flaw that it shouldn't memleak but might. To me it isn't either an "accidentally correct design" nor an "avoided leak" but ok. If something _has_ to be freed then it should be done explicitely either way hence crashing the GC for that particular ressource, I follow you here.

> Example 2:
>
> Resource A depends on Resource B. You forget to release either. The GC happens to call A and B destructors in the right order, by chance. A new D release changes this order later.

Here comes the accidentally correct design. Ok, I'm with you on that one.

I think however that this should really be limited on ressources that _must_ be freed in time. It shouldn't become a standard way to deal with the GC.

August 29, 2015
On Saturday, 29 August 2015 at 14:00:59 UTC, ponce wrote:
> On Saturday, 29 August 2015 at 13:43:33 UTC, cym13 wrote:
>> But nobody's linking the destructor to it because of the separation of concerns principle: we release what has to be released and only that: freeing the object is the realm of the GC.
>
> I see what you mean, but Unique!T, RefCounted!T and scoped!T call the destructor, not the release() function you just defined. So separating concerns break those.

Yes, and I think it is kind of cumbersome actually. Being able to pass a method to scoped!T for example would be really great (with the destructor as default of course).
August 29, 2015
On Saturday, 29 August 2015 at 13:14:26 UTC, ponce wrote:
> ...

All of this could be fixed by not letting the GC call destructors. It's a bad, error-prone design to begin with and I guarantee any semi-large D program is probably abusing undefined behavior due to it.
August 29, 2015
On Saturday, 29 August 2015 at 14:17:10 UTC, rsw0x wrote:
> On Saturday, 29 August 2015 at 13:14:26 UTC, ponce wrote:
>> ...
>
> All of this could be fixed by not letting the GC call destructors. It's a bad, error-prone design to begin with and I guarantee any semi-large D program is probably abusing undefined behavior due to it.

After reading all that, I too am convinced that the GC shouldn't call the destructor.
August 29, 2015
On 08/29/2015 04:20 PM, cym13 wrote:
> On Saturday, 29 August 2015 at 14:17:10 UTC, rsw0x wrote:
>> On Saturday, 29 August 2015 at 13:14:26 UTC, ponce wrote:
>>> ...
>>
>> All of this could be fixed by not letting the GC call destructors.
>> It's a bad, error-prone design to begin with and I guarantee any
>> semi-large D program is probably abusing undefined behavior due to it.
>
> After reading all that, I too am convinced that the GC shouldn't call
> the destructor.

But then classes with destructors shouldn't be allowed to be allocated on the GC heap in the first place, which is a PITA as well. (Note that classes/arrays can have destructors because some component structs have them; structs generally assume that their destructors will be called.)
August 29, 2015
On Saturday, 29 August 2015 at 14:32:27 UTC, Timon Gehr wrote:
> On 08/29/2015 04:20 PM, cym13 wrote:
>> On Saturday, 29 August 2015 at 14:17:10 UTC, rsw0x wrote:
>>> On Saturday, 29 August 2015 at 13:14:26 UTC, ponce wrote:
>>>> ...
>>>
>>> All of this could be fixed by not letting the GC call destructors.
>>> It's a bad, error-prone design to begin with and I guarantee any
>>> semi-large D program is probably abusing undefined behavior due to it.
>>
>> After reading all that, I too am convinced that the GC shouldn't call
>> the destructor.
>
> But then classes with destructors shouldn't be allowed to be allocated on the GC heap in the first place, which is a PITA as well. (Note that classes/arrays can have destructors because some component structs have them; structs generally assume that their destructors will be called.)

make classes with destructors(and structs allocated via new) have RC semantics.
« First   ‹ Prev
1 2 3 4