Below is a very simple console app (try the fiddle):
using System;
using System.Threading;
using System.Threading.Tasks;
public class ConsoleApp
{
class Callback
{
public Callback() { }
~Callback() { Console.WriteLine("~Callback"); }
}
static void Test(CancellationToken token)
{
Callback callback = new Callback();
while (true)
{
token.ThrowIfCancellationRequested();
// for the GC
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
Thread.Sleep(100);
}
// no need for KeepAlive?
// GC.KeepAlive(callback);
}
public static void Main()
{
var cts = new CancellationTokenSource(3000);
try
{
Test(cts.Token);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();
Console.WriteLine("Enter to exit...");
Console.ReadLine();
}
}
Here, the callback
object doesn't get garbage-collected until it goes out of scope of the Test
method.
I thought GC.KeepAlive(callback)
would be required to keep it alive inside Test
(as MSDN suggests), but apparently it is not (commented out in the code above).
Now if I change the code like below, the callback
does get garbage-collected, as expected:
Callback callback = new Callback();
callback = null;
This happens with .NET 4.5.1.
The question: Am I missing something? Can I rely upon this behavior, or is it something .NET version specific?
.NET garbage collection is non-deterministic.
The MSDN page to which you linked says it all really - emphasis added:
Just because
callback
can be garbage collected before it goes out of scope on return fromTest
does not mean that it will be.@Porges' comments explain everything very well:
The Release build and Ctrl-F5 brought back the expected behavior. I urge @Porges to post this as an answer, which I'd up-vote and accept with thanks.
As a follow-up, I'd like to feature the following interesting behavior. Now with Release + Ctrl-F5, even if I un-comment the
// GC.KeepAlive(callback)
line in my code, thecallback
still gets garbage-collected. Apparently, this is because the compiler recognizes this line as unreachable due towhile (true)
loop and still doesn't emit a strong reference oncallback
.The following is the correct pattern:
It's also interesting to look at the
GC.KeepAlive
implementation:As expected, it does nothing and merely servers as a hint to the compiler to generate IL code which keeps a strong reference to the object, up to the point where
KeepAlive
is called.MethodImplOptions.NoInlining
is very relevant here to prevent any optimizations like above.