www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - GC and destruction events

reply "Ben Hinkle" <bhinkle mathworks.com> writes:
Some recent posts got me thinking about the fact that the garbage collector
collects garbage in random order and so one can't rely on destructors being
run in any particular order. To work around this I've hacked up destructor
events. There is one interface and one mixin template. I've also included
two example classes A and B to illustrate how destructor events work to
protect a call to B.foo from A.~this. The code for A is a tad verbose but if
you think about it there isn't much else one can do. Here's the code:

alias void delegate() DtorCallback;
interface DtorEvent {
  void dtorCallback(DtorCallback cb);
  DtorCallback dtorCallback();
}
template DtorEventImpl() {
  DtorCallback dtorCallback_;
  DtorCallback dtorCallback() { return dtorCallback_; }
  void dtorCallback(DtorCallback cb) { dtorCallback_ = cb; }
  void fireDtorEvent(){ if (dtorCallback_) dtorCallback_(); }
}

class A {
 B b;
 this() {
   b = new B;
   b.dtorCallback = &deletingB;
 }
 ~this() {
   printf("A.~this\n");
   if (b) {  // b is being destroyed after A
     b.dtorCallback = null; // prevent b from called a deleted A
     b.foo();
   }
 }
 void deletingB() { // b is being destroyed before A
  printf("A.deletingB\n");
  if (b) {
    b.foo();
    b = null; // prevent ~this from calling a deleted B
  }
 }
}

class B : DtorEvent {
  mixin DtorEventImpl;
  ~this() { fireDtorEvent(); }
  void foo() { printf("B.foo\n"); }
}

int main() {
 A a = new A;
 // delete a;  // test explicit delete, too
 return 0;
}
Sep 14 2004
next sibling parent reply pragma <pragma_member pathlink.com> writes:
In article <ci7jem$86o$1 digitaldaemon.com>, Ben Hinkle says...
Some recent posts got me thinking about the fact that the garbage collector
collects garbage in random order and so one can't rely on destructors being
run in any particular order. To work around this I've hacked up destructor
events. There is one interface and one mixin template.

Ben, I like what you've done here. I coded something similar a while back that was in the same vein, did you take a look at this before? Then again, maybe wer're solving two different problems: http://svn.dsource.org/svn/projects/dsp/trunk/misc/systemFinalizer.d Maybe we could mesh our designs? I like the idea of having a mixin that can be dropped into a class to register it for deterministic finalization. Its something that D could really use. Also, I thought the problem was that destructors only run in an unpredictable order at program termination (if at all). Is this still the case, or is there a deeper problem during runtime? - Pragma [[ Eric Anderton at yahoo dot com ]]
Sep 14 2004
parent "Ben Hinkle" <bhinkle mathworks.com> writes:
"pragma" <pragma_member pathlink.com> wrote in message
news:ci7ko5$8pp$1 digitaldaemon.com...
 In article <ci7jem$86o$1 digitaldaemon.com>, Ben Hinkle says...
Some recent posts got me thinking about the fact that the garbage


collects garbage in random order and so one can't rely on destructors


run in any particular order. To work around this I've hacked up


events. There is one interface and one mixin template.

Ben, I like what you've done here. I coded something similar a while back

 was in the same vein, did you take a look at this before? Then again,

 wer're solving two different problems:

 http://svn.dsource.org/svn/projects/dsp/trunk/misc/systemFinalizer.d

They look to be related but slightly different. With your code a reference to the object being tracked is put in a static table so the object is never collected until program exit (and maybe not even then?). If one uses the AutoSystemFinalizer the objects will get collected after the scope is exited so at least then the objects don't sit around forever. With my code I was trying to keep all the references just between the object being tracked and the object doing the tracking so they can be collected at any time they both become garbage.
 Maybe we could mesh our designs?  I like the idea of having a mixin that

 dropped into a class to register it for deterministic finalization. Its
 something that D could really use.

I wasn't going to develop that code any more. It was just to illustrate how one could go about enforcing an order. It is slightly subtle but doable. If you want to expand/merge/whatever my posed code feel free to. I am tempted to try to add something like this to std.stream to make a BufferedStream automatically flush the buffer and close the backing stream in destructors but the whole DtorEvent thing needs the backing stream to fire the event and if someone has a Stream subclass that doesn't fire the event it messes up the whole thing. So for now I'm leaning towards forcing Stream users to always close their streams explicitly.
 Also, I thought the problem was that destructors only run in an

 order at program termination (if at all).  Is this still the case, or is

 deeper problem during runtime?

The unpredictable order is any time the GC runs. Since the GC always runs at exit people tend to see the problem at exit. The way the GC works is it marks all garbage and then loops over the marked memory and calls any destructors for a given memory block. It has no idea if one block of memory needs to be collected before another block.
Sep 14 2004
prev sibling parent reply Russ Lewis <spamhole-2001-07-16 deming-os.org> writes:
Perhaps we should just add functionality to the GC to cover this.  We 
could add a function to the GC that looks like this:

void gc.SetDestroyOrder(void *a,void *b);

This function would register a destruction ordering; a would always get 
cleaned up before b.  The function would throw an exception if this 
created a loop.  Then you could write classes like this:

class A {
   B b;
   this() {
     b = new B;
     gc.SetDestroyOrder(this,b);
   }
   ~this() {
     b.DoSomething();
   }
}
Sep 14 2004
next sibling parent Ben Hinkle <bhinkle4 juno.com> writes:
Russ Lewis wrote:

 Perhaps we should just add functionality to the GC to cover this.  We
 could add a function to the GC that looks like this:
 
 void gc.SetDestroyOrder(void *a,void *b);
 
 This function would register a destruction ordering; a would always get
 cleaned up before b.  The function would throw an exception if this
 created a loop.  Then you could write classes like this:
 
 class A {
    B b;
    this() {
      b = new B;
      gc.SetDestroyOrder(this,b);
    }
    ~this() {
      b.DoSomething();
    }
 }

yeah - that would be a better solution. I wonder why collectors in C# and Java don't have that ability? It seems like it would be pretty nice. I wonder if it is a performance thing.
Sep 14 2004
prev sibling parent reply Arcane Jill <Arcane_member pathlink.com> writes:
In article <ci7rqu$cah$1 digitaldaemon.com>, Russ Lewis says...
Perhaps we should just add functionality to the GC to cover this.  We 
could add a function to the GC that looks like this:

void gc.SetDestroyOrder(void *a,void *b);

I suspect that it would be more practical and flexible, long term, to stop thinking of /the/ garbage collecter - instead, think only of /a/ garbage collector. That is, the built-in GC may not necessarily be the only one. If you link with a DLL, maybe that DLL will have its own GC. Maybe that DLL wasn't even written in D, but some other language (maybe even a D++ of the future). Or maybe you'd like to write a custom allocator which uses its own GC for some specialized purpose. Right now, you can't do this, because the existing GC won't co-operate with other GCs. It believes itself to be the only one in existence. What makes more sense to me would be to separate out the function of garbage /management/ from the function of garbage /collection/. A garbage collector could allocate memory (by whatever own means) and then register that memory with the (unique) garbage manager. The GM would decide when a particular block of memory was unreachable, but instead of destructing/freeing it, all it would have to do is notify the particular GC which registered it that it was now unused. The particular GC which allocated it could then destruct/free it (and this in turn could be a two-stage process, if a custom allocator/deallocator had been used). So, back to...
void gc.SetDestroyOrder(void *a,void *b);

This function would register a destruction ordering; a would always get 
cleaned up before b.  The function would throw an exception if this 
created a loop.

That could now be implemented as a member function of /a/ (not the) garbage collector - perhaps even a subclass of a Phobos class called GarbageCollector. New GC => new functionality. Maybe this is getting way too sophisticated, but the DLL issue will come back to haunt us if we ignore it. Not everything wants to be statically linked, and there are sufficient problems with destruction events that systems programmers are going to want to take control of memory management somehow. Right now, we can do that only if we completely ignore the GC. It would be so much nicer to be able to co-operate with it. Arcane Jill
Sep 15 2004
parent Russ Lewis <spamhole-2001-07-16 deming-os.org> writes:
I think you make a lot of sense here.

Question: Should we have the capacity for different GMs?  Some objects 
may be subject to different types of garbage collection criteria, or 
some types of data may be more easily managed with a different type of 
collector (like a refcounting collector, or a by-hand collector). 
Imagine a collector tied into a windowing protocol; as long as the 
window is active, the windowing manager holds a virtual reference to the 
window object; when the window is destroyed, the virtual reference to 
the window object goes away and the window object MIGHT be collected.

Arcane Jill wrote:
 In article <ci7rqu$cah$1 digitaldaemon.com>, Russ Lewis says...
 
Perhaps we should just add functionality to the GC to cover this.  We 
could add a function to the GC that looks like this:

void gc.SetDestroyOrder(void *a,void *b);

I suspect that it would be more practical and flexible, long term, to stop thinking of /the/ garbage collecter - instead, think only of /a/ garbage collector. That is, the built-in GC may not necessarily be the only one. If you link with a DLL, maybe that DLL will have its own GC. Maybe that DLL wasn't even written in D, but some other language (maybe even a D++ of the future). Or maybe you'd like to write a custom allocator which uses its own GC for some specialized purpose. Right now, you can't do this, because the existing GC won't co-operate with other GCs. It believes itself to be the only one in existence. What makes more sense to me would be to separate out the function of garbage /management/ from the function of garbage /collection/. A garbage collector could allocate memory (by whatever own means) and then register that memory with the (unique) garbage manager. The GM would decide when a particular block of memory was unreachable, but instead of destructing/freeing it, all it would have to do is notify the particular GC which registered it that it was now unused. The particular GC which allocated it could then destruct/free it (and this in turn could be a two-stage process, if a custom allocator/deallocator had been used). So, back to...
void gc.SetDestroyOrder(void *a,void *b);

This function would register a destruction ordering; a would always get 
cleaned up before b.  The function would throw an exception if this 
created a loop.

That could now be implemented as a member function of /a/ (not the) garbage collector - perhaps even a subclass of a Phobos class called GarbageCollector. New GC => new functionality. Maybe this is getting way too sophisticated, but the DLL issue will come back to haunt us if we ignore it. Not everything wants to be statically linked, and there are sufficient problems with destruction events that systems programmers are going to want to take control of memory management somehow. Right now, we can do that only if we completely ignore the GC. It would be so much nicer to be able to co-operate with it. Arcane Jill

Sep 15 2004