[Doh! I am an idiot.. I am rooting the object right there in the code..]
I have code that works as expected in Release, but fails in Debug.
I have a Dictionary that contains WeakReference
instances to other objects. In Release, the dictionary “loses” its values as expected, once they are not referenced and collection occurs. However, in Debug, it doesn’t seem to happen…
Even in debug, I do see other WeakReference
getting collected in Debug, but the ones in the dictionary are not…
The code below shows this. Even when I add multiple Collects and delays between them (Task.Delay(100)
), it still does not go away.
Any idea how to force the WRs to get nulled? I don’t mind too much, but I have a test that tests for this and it will fail in Debug.
Here’s the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
DoIt();
Console.ReadLine();
}
private static async void DoIt()
{
string key = "k1";
var dict = new WeakItemDictionary<string, string>();
var s = dict.GetOrAdd(key, k => String.Concat("sdsdsd", "sdsdsdsdsd"));
RunFullGCCollection();
var found = dict.GetItemOrDefault(key);
Console.WriteLine(found == null ? "Object got collected" : "Object is still alive");
}
private static void RunFullGCCollection()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
/// <summary>
/// Creates a dictionary of weakly referenced object that will disapear when no longer in use.
/// Be careful when adding functions to the class - you need to take a bunch of scenarios into account.
/// See how GetOrAdd() works for more info.
/// </summary>
/// <typeparam name="K">Key of the dictionary</typeparam>
/// <typeparam name="V">Value type for the dictionary</typeparam>
public class WeakItemDictionary<K, V> where V : class
{
public const int CleanPassFrequency = 10;
private Dictionary<K, WeakReference<V>> _dictionary = new Dictionary<K, WeakReference<V>>();
private int _addCount = 0;
public V GetOrAdd(K key, Func<K, V> factory)
{
WeakReference<V> weakRef;
V value = null;
if (!_dictionary.TryGetValue(key, out weakRef))
{
value = factory(key);
weakRef = new WeakReference<V>(value);
_dictionary[key] = weakRef;
_addCount++;
}
// If the value is null, try to get it from the weak ref (to root it).
if (value == null)
{
value = weakRef.GetTargetOrDefault();
// If the value is still null, means the weak ref got cleaned. We need to recreate (again, rooted)
if (value == null)
{
value = factory(key);
weakRef.SetTarget(value);
_addCount++;
}
}
CleanIfNeeded();
return value;
}
public V GetItemOrDefault(K key)
{
WeakReference<V> weakRef;
V value = null;
if (_dictionary.TryGetValue(key, out weakRef))
{
value = weakRef.GetTargetOrDefault();
}
return value;
}
private void CleanIfNeeded()
{
Lazy<List<K>> keysToRemove = new Lazy<List<K>>(false);
foreach (var item in _dictionary)
{
if (item.Value.IsDead())
{
keysToRemove.Value.Add(item.Key);
}
}
if (keysToRemove.IsValueCreated)
{
foreach (var item in keysToRemove.Value)
{
_dictionary.Remove(item);
}
}
}
}
public static class Extensions
{
public static bool IsDead<T>(this WeakReference<T> weak) where T : class
{
T t;
bool result = !weak.TryGetTarget(out t);
return result;
}
public static T GetTargetOrDefault<T>(this WeakReference<T> weak) where T : class
{
T t;
bool result = !weak.TryGetTarget(out t);
return t;
}
}
}
Your
s
variable has a reference to the object. Note that your force a collection even though the DoIt() method has not finished executing yet and thes
variable is still stored in the activation frame of the method. That works when you run the Release build without a debugger attached, it makes the garbage collector efficient. But not when you debug. Otherwise one of the a core reasons why the Release configuration exists in the first place.The technical reason for this difference in behavior is explained in detail in this post.
Not something you should fret about, you only need to understand why it behaves differently. You can alter the outcome by setting
s
back to null before calling GC.Collect(). Or moving the dict.GetOrAdd() call into another method.According to MSDN:
Permits means: the garbage collector can collect the object, but it needn't. So you will never know exactly when it gets collected. Even if you run it in Release mode, there may be times where .NET does not collect it.
So even if you can make it work for this special situation at the moment with @Hans Passant's answer, you can never be sure whether or not it will always behave the same way. It can depend on physical RAM and other programs running at the same time and consuming more or less memory.