The other afternoon I ran into some nightmarish debugging with the following code:
private static void StartThreads()
{
var values = new List<int>() { 1, 2, 3 };
var threads = new List<Thread>();
foreach (var value in values)
{
Thread t = new Thread(() => Run(value));
threads.Add(t);
t.Start();
}
// Wait for all threads to complete
foreach (Thread t in threads)
t.Join();
}
private static void Run(int value)
{
Console.Write(value.ToString());
}
(I know, I know, I wish I could be using TPL but in this case I couldn't)
On my local machine, the code above ran and gave me my expected console output of 123
(your results may vary depending on what order the threads run in).
When I ran this code on my server however, the output was 333
.
begin pulling out hair
Long story short, after a couple hours of investigation I figured out that the way a foreach loop works under the hood in C# = 5.0, which is what I run on my local machine, works differently than a foreach loop in C# < 5.0, which is what I had on my server.
From the C# 4.0 spec, a foreach loop is really a while loop behind the scenes, meaning the code above really translates into something like this:
private static void StartThreads()
{
var values = new List<int>() { 1, 2, 3 };
var threads = new List<Thread>();
IEnumerator<int> e = ((IEnumerable<int>)values).GetEnumerator();
try
{
int v; // DECLARED OUTSIDE OF THE LOOP!!!
while (e.MoveNext())
{
v = (int)(int)e.Current;
Thread t = new Thread(() => Run(v));
threads.Add(t);
t.Start();
}
}
finally
{
if (e != null) ((IDisposable)e).Dispose();
}
// Wait for all threads to complete
foreach (Thread t in threads)
t.Join();
}
The important thing to note in the above code is that int v gets declared outside of the while loop.
In the C# 5.0 spec, that int v gets declared inside the loop (causing it to get recreated with every iteration):
private static void StartThreads()
{
var values = new List<int>() { 1, 2, 3 };
var threads = new List<Thread>();
IEnumerator<int> e = ((IEnumerable<int>)values).GetEnumerator();
try
{
while (e.MoveNext())
{
int v; // C# 5.0 DECLARED INSIDE THE LOOP
v = (int)(int)e.Current;
Thread t = new Thread(() => Run(v));
threads.Add(t);
t.Start();
}
}
finally
{
if (e != null) ((IDisposable)e).Dispose();
}
// Wait for all threads to complete
foreach (Thread t in threads)
t.Join();
}
Because my local machine and server were running different versions of .NET, the same exact code was producing totally different results.
Eventually I found Eric Lippert's article about the matter. Since I'm still fairly new to the world of .NET, I wasn't around for the big debate that took place in his comment's section regarding which should be the correct implementation. However, it is interesting to note that the C# devs decided to switch the logic on how the foreach loop operates so late in the game.
My eventual workaround for the .NET 3.5/C# 4.0 server was to assign the int to a newly created variable inside the foreach:
foreach (var value in values)
{
var tempValue = value; // THE FIX
Thread t = new Thread(() => Run(tempValue));
threads.Add(t);
t.Start();
}
As frustrating it may be to debug problems like this, it is nice to learn a little bit more of the language's history and idiosyncrasies.