In the code below, the Starter object is set to null, and GC.Collect() is called, but the background task still runs and the object isn't collected:
namespace ConsoleApp5
{
internal class Program
{
static void Main(string[] args)
{
Starter s = new();
s.Start();
s = null;
GC.Collect(0, GCCollectionMode.Forced);
GC.Collect(1, GCCollectionMode.Forced);
GC.Collect(2, GCCollectionMode.Forced);
Console.ReadLine();
}
}
public class Starter
{
int a = 0;
public void Start()
{
Task.Run(() =>
{
while (true)
{
a++;
Console.WriteLine(a);
}
});
}
}
}
In the code below, the Starter object is set to null, and GC.Collect() is called, but the background task still runs and the object isn't collected:
namespace ConsoleApp5
{
internal class Program
{
static void Main(string[] args)
{
Starter s = new();
s.Start();
s = null;
GC.Collect(0, GCCollectionMode.Forced);
GC.Collect(1, GCCollectionMode.Forced);
GC.Collect(2, GCCollectionMode.Forced);
Console.ReadLine();
}
}
public class Starter
{
int a = 0;
public void Start()
{
Task.Run(() =>
{
while (true)
{
a++;
Console.WriteLine(a);
}
});
}
}
}
Share
Improve this question
edited Jan 24 at 13:49
Cavid Haciyev
asked Jan 24 at 13:30
Cavid HaciyevCavid Haciyev
2991 silver badge10 bronze badges
5
|
2 Answers
Reset to default 4Thread invocation methods (and thus their targets) count as a GC root, so the moment Start()
is invoked, the object is rooted by the thread machinery. That thread is short-lived and only serves to create a thread-pool item - and almost certainly hasn't even started by the time GC.Collect()
executes (and completes), but even if it had finished: the thread-pool queue and active workers are also rooted (via the thread-pool machinery). Since a
is a field on the object, and the thread-pool callback touches that field: the thread-pool task can see the object, thus it is not collectable.
However, it is very likely that at the time GC.Collect()
executes, the ThreadStart
delegate is still in a pending queue waiting for the thread to be fully created and activated - but since the ThreadStart
has a target of the Starter
object you created: it is still reachable and thus not collectable.
Just to add to Marc Gravell's excellent answer a bit of simplified illustration based on the initial example:
internal class Program {
static void Main(string[] args) {
Starter s = new();
Console.WriteLine(s.GetHashCode());
s.Start();
s = null;
GC.Collect(0, GCCollectionMode.Forced);
GC.Collect(1, GCCollectionMode.Forced);
GC.Collect(2, GCCollectionMode.Forced);
Console.WriteLine("-----------");
ThreadInfrastructure.Repository
.FirstOrDefault().Invoke();
Console.ReadLine();
}
}
public static class ThreadInfrastructure {
public static List<Action> Repository = new();
}
public class Starter {
private int _a = 0;
public int a
{
get => _a;
set
{
Console.WriteLine(this.GetHashCode());
_a = value;
}
}
public void Start() {
Action action = () => {
a++;
Console.WriteLine(a);
};
ThreadInfrastructure.Repository.Add(action);
}
}
Possible output:
23458411
-----------
23458411
1
The ThreadInfrastructure
has a list which has a reference to the Action
which has a reference to our object. So it's not eligible for garbage collection because it is reachable.
Starter
, as it needs to incrementStarter.a
. – canton7 Commented Jan 24 at 13:34Task
: a thread won't just die because the GC did a collection. Threads keep objects alive, not the other way around – canton7 Commented Jan 24 at 13:36GC.Collect()
is called and completed; but the fact that the delegate (with theStarter
as the target) is in the queue is more than sufficient to keep the object alive. – Marc Gravell Commented Jan 24 at 13:36