Last active
April 13, 2021 00:36
An experimental implementation of .NET ConditionalWeakTable that allows collection of the table when value back-references the table.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
dotnet/runtime#12255 (comment)