Problem with finalizers


#1

I’m currently having some trouble with a finalizer cleaning up an object that is needed by another finalizer.

I have two objects, both of which contain pointers to some C object, one of which is a “context object” for the other (contains some information needed to clean the other up).

The first object R has a type that looks like this:

type SingularPolyRing
   ptr::some_c_thing1
end

The second object p say, has a type that looks like this:

type spoly
   ptr::some_c_thing2
   parent::SingularPolyRing

   function spoly(R::SingularPolyRing, p::some_c_thing2)
      z = new(p, R)
      finalizer(z, _spoly_cleanup)
      return z
   end
end

(Here _spoly_cleanup(z) calls some C function to clean up.)

Both objects have finalizers, but when julia is shut down, the finalizer for the first object R is always called before that of the second, even though the second object p contains a reference to the first object R!

I tried writing the finalizer for the spoly p as follows:

   function spoly(R::SingularPolyRing, p::some_c_thing2)
      z = new(some_c_thing2, R)
      finalizer(z, x -> _spoly_clear_fn(x, R))
      return z
   end

The reasoning is that surely the lambda/closure hangs onto a copy of R, preventing it from being cleaned up first. But alas, R is still cleaned up first, before _spoly_clear_fn gets called. I’ve verified this by putting print statements in the relevant C functions.

Is there a canonical way of preventing Julia from cleaning up an object that is needed to clean up another object?


#2

Finalizers can be called at any time after the object is dead in any order.


#3

Sure. But surely the object isn’t dead if something still has a reference to it.


#4

Not if the one referencing it is another dead object.


#5

So the solution seems to be that I need to maintain another reference to R somewhere. Presumably p is cleaned up because the REPL or some internal symbol table no longer has a reference to it.

Is there somewhere I can put R so that it is cleaned up after all the other global symbols, e.g. in Base or something like that?


#6

No. It’s unclear what exactly is the behavior your need but maintaining a refcount manually can be a solution if that’s what the C library want.


#7

On the C side, p is basically a tagged union. R is a context object that contains a lookup table of function pointers for all objects that are tagged unions like p. The “generic” delete function for p requires you to pass both p and R so that it can look up which delete function it is supposed to be calling to clean up p. It looks at the type tag for p to determine which function needs to be called.

Some libraries would just maintain a pointer to R inside p itself, but for extremely performance critical stuff, where memory usage is really important, p can’t hold a pointer to R and so you have to pass both p and R to delete. That means on the Julia side, the object R is needed to clean up p, which means you can’t clean up R first.

Anyway, I’ll see if I can find some information about maintaining refcounts. Perhaps a solution will suggest itself.


#8

Just Googling refcount Julia doesn’t seem to be turning up anything relevant. Is the idea that you can somehow assign a refcount to finalized objects? Where would I find this?


#9

It’s ok, I think I figured out what you mean. You meant to manually maintain a refcount to R from all objects p that depend on it. All the finalizer for R would do is decrease the refcount. And only when there are really zero references would the actual C delete function be called.

That should work ok, I think. I’ll give it a try.


#10

Correct. <20 characters limit …>


#11

This works fine. Thanks for the suggestion.

Just to document for others who find the thread, here is what I did:

  • Inside the Julia type for R I added an additional field “refcount”, which is initially set to 1.
  • The finaliser for R decrements the refcount field and if the refcount == 0 it calls the C delete function.
  • The finaliser for p first cleans up p by calling the relevant C delete function, then it calls the finaliser for R directly (which as mentioned decrements the refcount by one more and checks if it is time to clean up R).

#12

I don’t mean to horn in here, but I think this could cause a problem if you have more than one reference to an R running around. Say, like the following
x = R() y=x

Is the reference count incremented at the y=x call? If not, couldn’t you end up with x being garbage-collected out from under you?


#13

I think that’s ok. The finalizer for R can’t get called by the gc itself unless there are no references to it. That’s why the refcount starts at 1 and not zero. That final refcount is for R itself. In your example, only when x and y both die is the finalizer for the actual object called by the gc.

I checked that this seems to not be a problem, anyway.

We actually have to keep a global dictionary of all R’s anyway (since we need the R’s to be singletons), so the only way the final reference to R can be zeroed is if the dictionary gets cleaned up, which can only happen in our case upon exit from Julia. Still, I don’t think this is a problem even without the dictionary.