0

I would like to ask why this two approaches are different and returns two different set of values:

The first one which is correct in my opinion return values from 0 to 8 and works on different threads (LINQPad code):

void Main()
{
    var newTasks = Enumerable.Range(0, 9).Select(x => Task.Run(() => DoSomething(x)));

    Task.WhenAll(newTasks);
}    

public int DoSomething(int value)
{
    return value;
}

Second, which is not correct in my opinion which returns random values a specially 9 but also works on different threads.

void Main()
{
    var tasks = new List<Task<int>>();
    
    for (var index = 0; index < 9; index++)
    {
        var task = Task.Run(() => DoSomething(index));
        
        tasks.Add(task);
    }
    
    Task.WaitAll(tasks.ToArray());
}    

public int DoSomething(int value)
{
    return value;
}

Is it possible to modify the second one and get the result similar to first example?

Thanks for your answers.

  • Related: [Captured variable in a loop in C#](https://stackoverflow.com/questions/271440/captured-variable-in-a-loop-in-c-sharp) – Theodor Zoulias May 28 '21 at 06:52

1 Answers1

2

Is it possible to modify the second one and get the result similar to first example?

When you pass a variable or object into Task.Run(()=> DoSomething(index)) you capture a variable.

In a sense you're not just sending that variable as a value or object, but giving that thread direct access to the original variable. (This is highly generalized and has lots of nuances).

The problem you most likely encountering is that the CLR is attempting to capture index, which is an iterator who's scope can not leave it's boundaries unless any attempt to do so would remain on the stack(the same thread basically, again over generalized).

When the CLR 'captures' index it will only capture a random value that index was(due to race conditions) - or the last value that index was, which is double un-intuitive because you explicitly told it that index should never get to 9 in this line here for (var index = 0; index < 9; index++).

Here's the tricky thing, when the CLR captures index, it captures whatever the last value was for index. But after the loop ends index gets incremented one more time causing 9 to be the last value of index.

This can cause a whole host of problems, especially if you're working with arrays, here we come IndexOutOfBoundsException!

How to fix it
The fix is actually super simple! Just create a temporary variable in the loop, and pass that instead of index directly.

Here's an example:

void Main()
{
    var tasks = new List<Task<int>>();
    
    for (var index = 0; index < 9; index++)
    {
        int tmpIndex = index;

        var task = Task.Run(() => DoSomething(tmpIndex ));
        
        tasks.Add(task);
    }
    
    Task.WaitAll(tasks.ToArray());
}

Is it possible to modify the second one and get the result similar to first example?

Side Note
never rely on data from tasks to be in order, unless you explicitly implemented a guarantee that the items come back in order. The TaskScheduler chooses Tasks to run normally in FIFO(first in first out), however, if the TaskScheduler optimizes, prioritizes, or shifts the tasks around(which is does frequently) your tasks will not complete in order and subsequently the results you get won't be in order.

DekuDesu
  • 2,224
  • 1
  • 5
  • 19