1

In the following code, why we don't get NullReference exception and var2 value is 56 though the TestMethod has certainly finished before 'Messagebox' line? I read this great answer from Eric Lippert and this blog post, but I still don't get it.

void TestMethod()
{
    int var1 = 10;
    List<long> list1 = new List<long>();
    for (int i = 0; i < 5; i++)
        list1.Add(i);

    ThreadPool.QueueUserWorkItem(delegate
    {
        int var2 = var1;
        Thread.Sleep(1000);
        list1.Clear();
        MessageBox.Show(var2.ToString());
    });
    var1 = 56;
}
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
BHP
  • 443
  • 4
  • 13
  • The answer you're linking to of mine is about C, not C#. Managed memory languages don't have the problem of accessing a variable after its lifetime is over because the memory is *managed* so that this never happens. – Eric Lippert Sep 29 '18 at 08:53
  • You do not necessarily get that var2 is 56 in the new thread. You've written a *race condition*; the threads *race* to determine who gets to the assignment first, and you are not given any guarantees about which thread will win. – Eric Lippert Sep 29 '18 at 08:55
  • You say you are expecting a null dereference, but of *which reference do you think is null*? There are no null references in your program! – Eric Lippert Sep 29 '18 at 08:56
  • @EricLippert, regarding race condition, I got your point,but if we re-order the first 2 lines ( `int var2 = var1` and `Thread.Sleep` ) and let the caller method finishes it's work, we still get the same result. Maybe there is something Implementation-related here that I do not know. – BHP Sep 29 '18 at 09:22
  • @EricLippert, regarding null reference, I expected that when `TestMethod` method finished, It's stack was erased and so, the `list1` variable. – BHP Sep 29 '18 at 09:24

2 Answers2

3

I think it's because delegate has formed closure around variable var1. Probably looking at how closure works internally would help you. You can refer to explanation here

The compiler (as opposed to the runtime) creates another class/type. The function with your closure and any variables you closed over/hoisted/captured are re-written throughout your code as members of that class. A closure in .Net is implemented as one instance of this hidden class.

Having that that, I believe roughly compiler generated code would look like :

void TestMethod()
{
    UnspeackableClosureClass closure = new UnspeackableClosureClass(10);
    List<long> list1 = new List<long>();
    for (int i = 0; i < 5; i++)
        list1.Add(i);

    ThreadPool.QueueUserWorkItem(closure.AutoGeneratedMethod);
    closure.closureVar = 56;
}

public class UnspeackableClosureClass
{
   public int closureVar;
   public UnspeackableClosureClass(int val){closureVar=val}

   public void AutoGeneratedMethod(){
     int var2 = closureVar;
     Thread.Sleep(1000);
     list1.Clear();
     MessageBox.Show(var2.ToString());
  }
}
rahulaga-msft
  • 3,964
  • 6
  • 26
  • 44
  • 1
    Your guess is close to reality. For the actual generated source, have a look at [sharplab.io](https://sharplab.io/#v2:CYLg1APgAgDABFAjAbgLAChYMQFjZ+JAOgBUALAJwFMBDYASwDsBzfLYgdSeAHsB3AM5EAYjwoBbAfgxQAzAgBMcAMJwA3hjhaEOOCSoCALgFkqhsj2AAKAJQYN6bXCaG4ANxoVEcALxxEMPhOADL0RgA8ADY8LAB8cJFhht5+jFR8cKER0XG2QdoAZmJwVi7OvnCB5eFwAKzIzmBgdo5O2olGiEQAgsDW9DbSrVrk1HQACjw8kUQAigCuVIsAqgJUFBxiANYAkoZU4lbAVJFUzDT7mtoObdplHhRKfg8oV7ejtMBEAMqnVAAOVgCMBggzebQ6ySIylOnjy4KcpgEAhozCoACEeAAPH4WPhWB4KUg8b6GChMZi2MHDOAAX2pTheFVqADZ8LSMLSgA===) – germi Oct 01 '18 at 06:56
0

I think what you're saying is that you expect var1 to be deallocated when TestMethod() exits. After all, the local variables are stored on the stack, and when the method exits, the stack pointer has to revert to the spot where it was before the call, meaning that all the local variables are deallocated. If that were really what were happening, var1 might not be set to null at all; it could contain garbage, or bits of some other local variable, created later when the stack pointer moves again. Is that what you mean?

What turned the light on for me is the understanding that asynchronous thinking is not stack-based at all. A stack just doesn't work-- because the order of calls do not form a stack. Instead, bits of code are associated with contextual objects which are held on the heap. They can execute in any order and even simultaneously.

Your delegate needs var1, so the compiler promotes it from a variable held in the stack to a variable held in one of these objects, associated with the delegate's behavior. This is what is called a "closure" or a "closed variable." To the delegate, it looks just like a local variable, because it is-- just not on the stack any more. It will live as long as that object needs to live, even after TestMethod() has exited.

John Wu
  • 50,556
  • 8
  • 44
  • 80
  • **Is that what you mean?** Yes. Exactly. Now I have better understanding of this behavior. – BHP Sep 29 '18 at 09:29
  • The correct way to think about this is to stop thinking that "local means stack". **Short lifetime means stack**. "Local" is a property of the **name** of the thing; "local" means that the name of this variable is only meaningful in the *locality* of the variable. Also, stop thinking of the stack as "the stack". Think of it as the *short term allocation pool*. Now what is our correct characterization? **Short lifetime variables go on the short term allocation pool**. That is *obviously correct*, which is what we want in an explanation. – Eric Lippert Sep 29 '18 at 15:02
  • 1
    Since a local variable used in an anonymous function has *the same lifetime as the delegate*, and since the delegate is *possibly long-lived*, we cannot put the local on the *short term pool*, so we don't. We put it on the long-term allocation pool, and it lives until the garbage collector deallocates it. – Eric Lippert Sep 29 '18 at 15:03