1

I'm trying to understand why the following program gives the output that it does. I know it has something to do with references and values but I neither know the terminology nor do I know where to go to learn more.

        for (int x = 0; x < 2; x++)
        {
            int y = x;
            new Thread(new ThreadStart(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine("Thread sees x = {0}, y = {1}", x, y);
                })).Start();
        }
        Thread.Sleep(1000);

Output:

Thread sees x = 2, y = 0
Thread sees x = 2, y = 1

A reference explaining this sort of thing would be very much appreciated.

Chris Merck
  • 454
  • 3
  • 12

3 Answers3

2

NOTE: This is not actually the full story, and I probably got something wrong, but the idea still stands.

This happens because of closures.

Closures happen in for loops (and used to happen in foreach loops, but that was changed in C# 5). What gets compiled is something like this:

int x = 0;
while (x < 2)
        {
            int y = x;
            new Thread(new ThreadStart(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine("Thread sees x = {0}, y = {1}", x, y);
                })).Start();
            x++;
        }
        Thread.Sleep(1000);

As x exists outside the scope of the for loop, the lambda does not take in. So it's still x. However, as y's scope ends outside the for loop, the lambda HAS to keep the variable, it can't just get it later, when it runs, as it'd be gone by then. So in fact, the lambda is something like this at runtime:

First loop run:

() => Thread.Sleep(100); Console.WriteLine("Thread sees x = {0}, y = {1}, x, 0);

Second loop run:

() => Thread.Sleep(100); Console.WriteLine("Thread sees x = {0}, y = {1}, x, 1);

And by the time the lambda actually runs, x is already 2, and as it didn't take it in, it reads it as 2.

It'sNotALie.
  • 22,289
  • 12
  • 68
  • 103
0

This is how closure works in C# 4.0 and older. The variable x is more intuitive in C# 5.0 (indeed for foreach, not for loops as pointed out).

Closure captures the same variable each iteration. So you end up with the last value.

        List<Action> actions = new List<Action>();
        for (int i = 0; i < 10; i++)
        {
            Action anonymousFunction = () => Console.WriteLine(i);                
            actions.Add(anonymousFunction);
        }
        foreach (var action in actions)
        {
            action();//prints out the last value every time
        }

However you can change the behavior by copying a new value.

        List<Action> actions = new List<Action>();
        for (int i = 0; i < 10; i++)
        {
            int copyOfOriginalValue = i;
            Action anonymousFunction = () => Console.WriteLine(copyOfOriginalValue);                
            actions.Add(anonymousFunction);
        }
        foreach (var action in actions)
        {
            action();//prints out unique values
        }
P.Brian.Mackey
  • 43,228
  • 68
  • 238
  • 348
  • 2
    Only in foreachs, still works the same as C# 4 for for loops. – It'sNotALie. Mar 13 '13 at 19:47
  • Again, for whoever keeps downvoting me, please explain why – P.Brian.Mackey Mar 13 '13 at 19:55
  • 1
    @P.Brian.Mackey 1) The answer was wrong, and even now, is rather misleading, 2) There wasn't "no comment". There was in fact a comment explaining an important error with your post 3) closure semantics never changed at all in C# 5.0 over any previous version, all that changed is the scope of the loop variable in a `foreach` loop. As the OP's code has none of those, it's not relevant here. 4) You never actually do explain what a closure means/does, all you do is provide a code snippet that demonstrates the behavior, which is what the OP has in his code snippet. He wants it explained. – Servy Mar 13 '13 at 19:58
  • @Servy - There is nothing wrong with my answer. You keep attacking my questions and answers and I don't appreciate it. You need to stop. You downvote my answers while they are still being actively written. – P.Brian.Mackey Mar 13 '13 at 19:59
  • @P.Brian.Mackey There was, at the time the downvote was given. You stated that `for` loop closure semantics changed in C# 5.0. They did not. Note my previous comment uses the past tense, it *was* wrong, it is now no longer wrong, but it is misleading, in that it still implies that closure semantics have somehow changed with C# 5.0. They have not, at all. I comment and downvote on incorrect or otherwise problematic answers when I see them. I don't spend time looking at users. If you don't have problems with posts, or I don't notice them, then I won't comment. Don't take it personally. – Servy Mar 13 '13 at 20:02
  • I am referring to the fact that to a programmer the output *did* change. The programmer typically does not care how it was implemented and should not care. That is implementation detail filed away in the spec. I'm not rewriting the spec here. Eric Lippert's annotations bring this out as a change in the 4.0 book he annotated. – P.Brian.Mackey Mar 13 '13 at 20:04
  • @P.Brian.Mackey You mean you write a skeleton of an answer, and then actually edit in the actual "answer" later, just so you can get more upvotes? – It'sNotALie. Mar 13 '13 at 20:05
  • @P.Brian.Mackey No, the code provided by the OP will not change, in any way, from C# 4.0 to C# 5.0. Stating that it does is wrong, implying that it does is misleading. Additinoally, the OP is *specifically asking for an explanation for how it works*. If you don't care how it works and just want to know what it does, then perhaps you shouldn't try to answer a question asking specifically for that type of explanation. – Servy Mar 13 '13 at 20:06
  • @Servy - You are arguing a point I am not even trying to make. – P.Brian.Mackey Mar 13 '13 at 20:07
  • @P.Brian.Mackey It's what you *said* before your edit, which was wrong. After the edit it's implied, but not stated, so it's misleading even though it's not wrong. The point is you're not answering the question, and instead discussing an entirely unrelated issue that is at best not helpful, and at worse confusing the unrelated issue with the current one. – Servy Mar 13 '13 at 20:17
  • @Servy - Your point is not objective and not worth these continous downvotes. If you have an issue with something then be more constructive by offering feedback. Don't just discourage people trying to give free help by slapping out downvotes with no useful feedback. With as much reputation as you have you should learn to be more helpful to fellow answerers. – P.Brian.Mackey Mar 13 '13 at 20:19
  • I have offered feedback. What do you think [this](http://stackoverflow.com/questions/15394999/meaning-of-an-external-variable-used-within-a-lambda/15395038?noredirect=1#comment21762700_15395038) is? If I downvoted without commenting at all that would be not offering feedback. You asked for more feedback, and I've given it to you, and now you're getting upset because I'm detailing all of the problems with your post. Which do you want, do you want me to describe all of the ways in which this post is problematic and unhelpful, or do you want me to stick with the anon downvotes? – Servy Mar 13 '13 at 20:25
  • I'm asking you to be helpful by offering constructive feedback. That way there can be an open discussion about your thoughts and answers such as myself can learn why it is that you disagree. That doesn't require a downvote. Downvotes should be used with more discretion. You throw them out over pedantry. – P.Brian.Mackey Mar 13 '13 at 20:29
  • 1
    I don't think a crucial failure in an answer is pedantry... – It'sNotALie. Mar 13 '13 at 20:30
  • @P.Brian.Mackey When I see an answer that is a) wrong and b) doesn't answer the question at all, I will downvote it. I may also comment on the answer to help the poster improve it, and I frequently do. If the problems addressed in the comments are fixed (and I notice it) then I'll reverse the downvote. **That's what downvotes are for**, to indicate incorrect and/or unhelpful answers. This fits both criteria. If I'm going to comment on an answer to explain how it's wrong, (unless the mistake is small and not important) I'll downvote. – Servy Mar 13 '13 at 20:35
  • If the downvote bothers you then consider *fixing the post* rather than criticizing the person who you think gave it to you. – Servy Mar 13 '13 at 20:36
0

You're passing an anonymous method / lambda to the constructor of the ThreadStart object. The compiler creates a class to contain this anonymous method, and elevates x and y from being local variables in the method to class variables in this compiler-created class. That's why it is able to access the values of those variables.

See this MSDN article: http://msdn.microsoft.com/en-us/library/0yw3tz5k(v=vs.80).aspx

Dan
  • 9,717
  • 4
  • 47
  • 65