Executing the following code:
import os
type MyRef = ref object of RootObj
value: int
proc finalizer(myref: MyRef) =
echo "finalizer was called"
var myref: MyRef
echo "Use new with finalizer to allocate"
new(myref, finalizer)
myref.value = 100
assert(myref.value == 100)
echo "initialization done"
sleep(1000)
echo "nullify the reference"
myref = nil
assert(myref == nil)
echo "sleep for a bit"
sleep(1000)
echo "Done"
gives the following output:
Use new with finalizer to allocate
initialization done
nullify the reference
sleep for a bit
Done
So it seems that the finalizer for the heap object wasn't called when its only reference was invalidated. How do I ensure that it does get called?Thanks, it worked.
Is that the only way to ensure that the finalizer gets called promptly when the reference count hits zero?
I understand your point. However, there are situations where it is important that a finalizer (or equivalent) is guaranteed to be called when a thread is no longer using a resource.
For me, the use case is shared heap memory. I am attempting to implement a framework that manages heap memory to be shared among threads, without the need for a global GC:
The point here is that it is must be guaranteed that when a client thread is no longer using a shared object, the count of referencing threads for that object is decremented. Ideally, that would also include the case where a client thread terminates without explicitly disposing of the shared object. I had hoped to use the existing GC heap allocation/finalization mechanism for that purpose, but that doesn't appear to be possible at this time. I suppose I could layer another type of reference-counted pointer on top of what Nim already has, but that seems more than a bit redundant, and the current inability to override the assignment operator would make it cumbersome/confusing to use.
It would be very nice if one could tag an object (or type?) for guaranteed (preferably eager) finalization - note that I am not including memory recovery in this, that can usually be deferred. I have seen discussions elsewhere that revolve around this need in other GC'ed languages, but I have yet to see a solution.
If there is some other way to accomplish what I need within the existing Nim framework, I would love to learn about it.
Agree with the first part of your comment, I need guaranteed and eager finalization.
Regarding the second part of your comment (if I understand it correctly): the GC of the client thread doesn't need to know how much memory is being managed for it. The client thread GC just manages proxy objects, each containing a pointer, on its own heap. The managing thread GC manages the shared objects, together with a count of referencing client threads (actually a count of proxy objects) on its heap. When a client thread allocates a proxy object on its heap, the initializer increments the shared object's proxy object count via dereferencing. When a client thread disposes of a proxy object, the proxy object's finalizer (hypothetically) decrements the shared object's proxy object count. When the proxy object count hits zero the finalizer sends a message to the managing thread to dispose of the actual shared object.
All nice and tidy, if the proxy object finalizer actually gets called.
You need neither guaranteed nor eager finalization; you need only an upper bound on unreachable memory.
And yes, I'm fully familiar with the scheme you are trying to implement.
The fact that stack scanning is conservative already makes it impossible to make any guarantees about finalization.
@Varriount Thanks. I agree that polling should work to detect a zero reference count, and would be a good alternative mechanism for the managing thread to clean up shared objects on its heap.
However, from the discussion so far it seems that the central issue is how to guarantee that the reference count gets updated when a client thread is no longer using the shared object.
lou15b: So, if I understand you correctly, it isn't possible to implement GC'd shared heap unless the GC is global (i.e. stop-the-world)?
No, I am saying that your assumptions for what is needed are too strong.
I think I understand Jehan's posts here. Let me illustrate it with an example:
Let's allocate some memory with new. If we delete all the references to one of these memory regions, then the compiler needs some time to realize that it can be freed (it will happen only at one of the next allocations when the GC is triggered). And we are ok with that.
So let's go back to your example. There are several threads, and your reference counter is not decreased immediately, just a little bit later, so you can't free the memory as soon as all the references are deleted. Maybe that's not end of the world (because it is similar to the previously described case), and your solution is still useful.