I'm writing a class which is roughly analogous to a CancellationToken, except it has a third state for "never going to be cancelled". At the moment I'm trying to decide what to do if the 'source' of the token is garbage collected without ever being set.
It seems that, intuitively, the source should transition the associated token to the 'never cancelled' state when it is about to be collected. However, this could trigger callbacks who were only kept alive by their linkage from the token. That means what those callbacks reference might now in the process of finalization. Calling them would be bad.
In order to "fix" this, I wrote this class:
public sealed class GCRoot {
private static readonly GCRoot MainRoot = new GCRoot();
private GCRoot _next;
private GCRoot _prev;
private object _value;
private GCRoot() {
this._next = this._prev = this;
}
private GCRoot(GCRoot prev, object value) {
this._value = value;
this._prev = prev;
this._next = prev._next;
_prev._next = this;
_next._prev = this;
}
public static GCRoot Root(object value) {
return new GCRoot(MainRoot, value);
}
public void Unroot() {
lock (MainRoot) {
_next._prev = _prev;
_prev._next = _next;
this._next = this._prev = this;
}
}
}
intending to use it like this:
Source() {
...
_root = GCRoot.Root(callbacks);
}
void TransitionToNeverCancelled() {
_root.Unlink();
...
}
~Source() {
TransitionToNeverCancelled();
}
but now I'm troubled. This seems to open the possibility for memory leaks, without actually fixing all cases of sources in limbo. Like, if a source is closed over in one of its own callbacks, then it is rooted by the callback root and so can never be collected.
Presumably I should just let my sources be collected without a peep. Or maybe not? Is it ever appropriate to try to control the order of finalization, or is it a giant warning sign?