views:

114

answers:

6

I have a snippet of code that I though should work because of closures. However the result proves otherwise. What is going on here to not produce the expected output, one of each word.

Code:

string[] source = new string[] {"this", "that", "other"};
List<Thread> testThreads = new List<Thread>();
foreach (string text in source)
{
    testThreads.Add(new Thread(() =>
    {
        Console.WriteLine(text);
    }));
}

testThreads.ForEach(t => t.Start())

Output:

other
other
other
+6  A: 

This has to do with the fact that closures capture the variable itself without evaluating it until it's actually used. After the end of the foreach loop the value of text is "other", and it as after the loop ends that the method is invoked, and at the time of invocation the value of the captured variable text is "other"

See this blog post from Eric Lippert for details. He explains the behavior and some of the reasons behind it.

Davy8
More specifically, a closure is only executed when invoked, not when declared. By the time it is invoked, the statement Davy8 makes is what happens.
spoulson
Edited to hopefully be more clear.
Davy8
+2  A: 

Closures in C# don't capture the value of text at time of creation. Since the foreach loop finishes execution before any of the threads execute, the last value of text is given to each.

This can be remedied:

string[] source = new string[] {"this", "that", "other"};
List<Thread> testThreads = new List<Thread>();

foreach (string text in source)
{
    // Capture the text before using it in a closure
    string capturedText = text;

    testThreads.Add(new Thread(() =>
        {
            Console.WriteLine(capturedText);
        }));
}

testThreads.ForEach(t => t.Start());

As you can see, this code "captures" the value of text inside of each iteration of the for loop. This guarantees that the closure gets a unique reference for each iteration rather than sharing the same reference at the end.

Justin Niessner
A: 

The reason this is happening is that by the moment you are starting your threads the loop has finished and the value of the text local variable is "other", so when you start the threads that's what gets printed. This could be easily fixed:

string[] source = new string[] {"this", "that", "other"};
foreach (string text in source)
{
    new Thread(t => Console.WriteLine(t)).Start(text);
}
Darin Dimitrov
A: 

Others have explained why you're running into this problem.

Luckily, the fix is very easy:

foreach (string text in source)
{
    string textLocal = text; // this is all you need to add
    testThreads.Add(new Thread(() =>
    {
        Console.WriteLine(textLocal); // well, and change this line
    }));
}
Dan Tao
+2  A: 

This is a classic mistake of capturing a loop variable. This affects both for and foreach loops: assuming a typical construction, you have a single variable across the whole duration of the loop. When a variable is captured by a lambda expression or an anonymous method, it's the variable itself (not the value at the time of capture) which is captured. If you change the value of the variable and then execute the delegate, the delegate will "see" that change.

Eric Lippert goes into great detail on it in his blog: part 1, part 2.

The usual solution is to take a copy of the variable inside the loop:

string[] source = new string[] {"this", "that", "other"};
List<Thread> testThreads = new List<Thread>();
foreach (string text in source)
{
    string copy = text;
    testThreads.Add(new Thread(() =>
    {
        Console.WriteLine(copy);
    }));
}

testThreads.ForEach(t => t.Start())

The reason this works is that each delegate will now capture a different "instance" of the copy variable. The variable captured will be the one created for the iteration of the loop - which is assigned the value of text for that iteration. Lo and behold, it all works.

Jon Skeet
A: 

Closures / lambdas cannot properly bind to foreach or loop counter variables. Copy the value to another local variable (not declared as a foreach or counter variable) and it will work as expected.

mihi