Skip to content

Instantly share code, notes, and snippets.

@acaly
Last active April 13, 2021 00:36
Show Gist options
  • Save acaly/380fb7ee48998983384ff10107a40e78 to your computer and use it in GitHub Desktop.
Save acaly/380fb7ee48998983384ff10107a40e78 to your computer and use it in GitHub Desktop.
An experimental implementation of .NET ConditionalWeakTable that allows collection of the table when value back-references the table.
using NUnit.Framework;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Threading;
namespace RealConditionalWeakTableTests
{
public class Tests
{
//=============================
//Part 1: DoubleDependentObject
//=============================
private class DoubleDependentObject
{
private GCHandle _track1, _track2;
public readonly object Load;
private DoubleDependentObject(object track1, object track2, object load)
{
_track1 = GCHandle.Alloc(track1, GCHandleType.WeakTrackResurrection);
_track2 = GCHandle.Alloc(track2, GCHandleType.WeakTrackResurrection);
Load = load;
}
private bool TryResurrect()
{
if (_track1.Target is null || _track2.Target is null)
{
_track1.Free();
_track2.Free();
return false;
}
return true;
}
//This object creates a new holder for the Object and keeps it alive every time itself dies.
private class DependentObjectResurrector : CriticalFinalizerObject
{
public DoubleDependentObject Object;
public DependentObjectResurrector()
{
}
~DependentObjectResurrector()
{
if (!Object.TryResurrect())
{
//Really dies.
return;
}
var newHolder = new DependentObjectResurrector()
{
Object = Object,
};
GC.KeepAlive(newHolder);
}
}
public static ReferenceHolder Create(object track1, object track2, object load)
{
var obj = new DoubleDependentObject(track1, track2, load);
var holder = new ReferenceHolder()
{
Holder = new(obj, true),
};
GC.KeepAlive(new DependentObjectResurrector() { Object = obj });
return holder;
}
}
private class ReferenceHolder
{
public WeakReference<DoubleDependentObject> Holder;
}
//NoInlining to avoid leaking temporary objects on stack.
[MethodImpl(MethodImplOptions.NoInlining)]
private static ReferenceHolder CreateDependentObject(object table, object key)
{
return DoubleDependentObject.Create(table ?? new object(), key ?? new object(), null);
}
[Test]
public static void NotAlive()
{
var holder = CreateDependentObject(null, null);
for (int i = 0; i < 10; ++i)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
var alive = holder.Holder.TryGetTarget(out _);
Assert.IsFalse(alive);
}
[Test]
public static void AliveWithOne()
{
var table = new object();
var holder = CreateDependentObject(table, null);
for (int i = 0; i < 10; ++i)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
var alive = holder.Holder.TryGetTarget(out _);
Assert.IsFalse(alive);
}
[Test]
public static void AliveWithTwo()
{
var table = new object();
var key = new object();
var holder = CreateDependentObject(table, key);
for (int i = 0; i < 10; ++i)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
var alive = holder.Holder.TryGetTarget(out _);
Assert.IsTrue(alive);
}
//=============================
//Part 2: WeakTable
//=============================
private class WeakTable
{
public readonly ConditionalWeakTable<object, ReferenceHolder> Entries = new();
public void Set(object key, object value)
{
Entries.AddOrUpdate(key, DoubleDependentObject.Create(key, this, value));
}
public bool TryGet(object key, out object value)
{
if (Entries.TryGetValue(key, out var refHolder) &&
refHolder.Holder.TryGetTarget(out var ddo))
{
value = ddo.Load;
return true;
}
value = null;
return false;
}
}
private class CounterObject
{
public static int Count;
public static void Clear()
{
Count = 0;
}
public CounterObject()
{
Interlocked.Increment(ref Count);
}
~CounterObject()
{
Interlocked.Decrement(ref Count);
}
}
[Test]
public static void TableAndKeyAlive()
{
CounterObject.Clear();
var table = new WeakTable();
var key = new object();
table.Set(key, new CounterObject());
for (int i = 0; i < 10; ++i)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Assert.True(table.TryGet(key, out var val));
Assert.IsNotNull(val);
Assert.AreEqual(1, CounterObject.Count);
}
[Test]
public static void TableAliveKeyDead()
{
CounterObject.Clear();
var table = new WeakTable();
[MethodImpl(MethodImplOptions.NoInlining)]
static void Setup(WeakTable table)
{
var key = new object();
//The load is a boxed ValueTuple that references both key and table.
table.Set(key, (key, table, new CounterObject()));
}
for (int i = 0; i < 10; ++i)
{
Setup(table);
}
for (int i = 0; i < 10; ++i)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Assert.AreEqual(0, CounterObject.Count);
}
[Test]
public static void TableDeadKeyAlive()
{
CounterObject.Clear();
var key = new object();
[MethodImpl(MethodImplOptions.NoInlining)]
static void Setup(object key)
{
var table = new WeakTable();
//The load is a boxed ValueTuple that references both key and table.
table.Set(key, (key, table, new CounterObject()));
}
for (int i = 0; i < 10; ++i)
{
Setup(key);
}
for (int i = 0; i < 10; ++i)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Assert.AreEqual(0, CounterObject.Count);
}
}
}
@acaly
Copy link
Author

acaly commented Apr 12, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment