I've been playing around with the following piece of code:
class RunMeBaby
{
public void Start()
{
while (true)
{
Console.WriteLine("I'm " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
}
}
}
class Program
{
static void Main(string[] args)
{
RunMeBaby r = new RunMeBaby();
Thread t = new Thread(r.Start); // ParameterizedThreadStart delegate
r = null;
GC.Collect(GC.MaxGeneration);
t.Start();
r = new RunMeBaby();
t = new Thread(() => r.Start()); // ThreadStart delegate
t.Start();
//Thread.Sleep(1000);
r = null;
}
}
While the first part of main is executed without a hitch, the second part fails when I comment the call to the Thread.Sleep()
method, I get a null exception.
My understanding is that the lambda expression being lazily evaluated, it can happen that the new thread isn't started fast enough and the main one sets r
to null
first. Now I put this "second part" in a static method with r
having a local scope, and the problem disappeared. But I wonder whether or not the problem is hidden by the thread scheduler in that particular case, maybe on a different machine with a different workload it could still occur. Or is there something about the lambda expression that guarantees that even though r
falls out of scope, as long as it hasn't been set to null
, it is still referenced somehow.
And ultimately I wonder whether I should consider using the ParameterizedThreadStart
delegate as much as possible or stick to the lambdas given I respect certain conditions to keep them valid.
Before we talk about garbage collection, first let's understand the code you have written.
There is a huge difference between:
new Thread(r.Start)
which creates a delegate for the Start
method on the current value of r
, i.e.
new Thread(new ThreadStart(r.Start)) // identical to new Thread(r.Start)
in either of the above, r
is evaluated now, so later changes to another instance (or null) won't affect it. Contrast with:
new Thread(() => r.Start())
this is an anonymous method with captures the variable r
, i.e. r
is only evaluated when the anonymous method is invoked, i.e. when the second thread is running. Consequently, yes: if you change the value of r
you could very well get a different result (or an error if you change it to null).
Parameterised thread start would work too:
new Thread(state => ((RunMeBaby)state).Start(), r);
which passes the current value of r
as the parameter value, so that is now fixed; when the delegate is invoked, state
gets the value that was (previously) in r
at the time, so you can cast that to the appropriate type and use it safely.
Now! In terms of garbage collection, there is nothing special to know. Yes, passing the reference r
into a ParameterizedThreadStart
will create a copy of the reference (not a copy of the object), so will prevent garbage collection until it is no longer in scope. The same, however, is also true of the original new Thread(r.Start)
approach. The only time it gets tricky is the "captured variable" example (() => r.Start()
), although the problem you are seeing has nothing whatsoever to do with GC, and everything to do with the rules of captured variables.
A lot of questions of which I don't understand all. What I can say is that
Thread t = new Thread(r.Start)
creates a delegate which will internally point to the instance that provides the method implementation to be called through the delegate. Hence, setting r to null will have no effect as there is a relationship of Thread -> ParameterizedDelegate -> your method -> RunMeBaby.
r = new RunMeBaby();
t = new Thread(() => r.Start());
creates a closure around r, which will still allow you to modify r. If you set it to null and access it a a later point, you get your NRE.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With