Implementing Phantom Reference in C#
I run into this very interesting blog post and I decided to see if I could implement this on my own, without requiring any runtime support. This turned out the be surprisingly easy, if you are willing to accept some caveats.
I’m going to assume that you have read the linked blog post, and here is the code that implement it:
Here is what it gives you:
- You can have any number of queues.
- You can associate an instance with a queue at any time, including far after it was constructed.
- Can unregister from the queue at any time.
- Can wait (or easily change it to do async awaits, of course) for updates about phantom references.
- Can process such events in parallel.
What about the caveats?
This utilize the finalizer internally, to inform us when the associated value has been forgotten, so it takes longer than one would wish for. This implementation relies on ConditionalWeakTable to do its work, by creating a weak associating between the instances you pass and the PhantomReference class holding the handle that we’ll send to you once that value has been forgotten.
Comments
I don't believe there is a guarantee the PhatomReference finalizer will be called before its
THandle _handle
is GCed, so it's possible you will passnull
to_queue
.Alternative design that does not rely on finalizers, but is O(n), is to have a list of
Tuple<Weakreference, THandle>
.Poll()
will loop through this list and remove the handles who's accompanying weakreference no longer has a value.Unregister()
would also have to loop through the list.Patrick, it can't be null, because PhatomReference is still alive. FReacheable queue keeps it alive until finalization occurs.
But idk if I like Phantom Reference in general. The problem it solves shouldn't happen, to begin with I've never had to implement my own finalizer, SafeHandle cover most of the cases. And when we have Finalizers, most of the times we are able to use Dispose method instead of it
If we count on devs making crazy things in Finalizers, we are just moving the problem to another place
Patrick,
_handle
is meant to be a non finalizable type. It can either be a real class, or typically anint
/IntPtr
or some such. It is guaranteed to not be set to null in the finalizaer, so no issue thereFelipe, If you are using native API, finalizer pop up a lot more often than you would like. In particular, they are a great way to complement (not replace) Dispose.
Felipe and Oren, you're right. My mental model was: a finalizer is part of GC, so any field can be null when the finalizer runs. But it looks like it doesn't even matter if
_handle
has a finalizer itself, because it would still not become null. No guarantees on which finalizer would run first though.Patrick, Yes, the finalizers may run in any order, but the CLR will not null references behind your back
It's a mind opening exercise and I need some with and hands on experience on the code. This code deserves a repo with tests included, I believe. Although it's not a great addition, I felt the urge to warn about the typo in the code. "Pathom" is used in the code instead of "Phantom".
Zafer, Feel free to create such a repo and tests. Fixed the typo, thanks
Oren, sorry... I couldn't resist. :)
Comment preview