1

I am just trying to learn the different ways of doing threading/tasks and I wanted a way to dynamically change the tasks that were being done and I was pointed to the Parallel.ForEach loop. I made a little example program and I have a few questions.

public void StartTest()
{
    List<Action> actions = new List<Action>();
    for (int i = 0; i < 6; i++)     
    {
        actions.Add(() => Function1("Word: " + i));
    }
    Parallel.ForEach(actions, new ParallelOptions
    {
        MaxDegreeOfParallelism = 2
    }, action => action());

    Console.WriteLine("Finished. \nTime Taken: " + total.ToString(@"dd\.hh\:mm\:ss"));
    Console.Read();
}


private void Function1(string word)
{
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine(word + " |  Task Id: " + Task.CurrentId + " |   " + i);
    }
    Console.WriteLine(word + " ----- Completed.");
}

So my first question is what does the "action => action()" chunk of the loop do? I understand what lambdas are but I really am just not following this.

My second question is why is this the output?

Word: 6 | Task Id: 3 | 0

Word: 6 | Task Id: 3 | 1

Word: 6 | Task Id: 3 | 2

Word: 6 | Task Id: 3 | 3

Word: 6 | Task Id: 3 | 4

Word: 6 ----- Completed.

Word: 6 | Task Id: 3 | 0

Word: 6 | Task Id: 3 | 1

Word: 6 | Task Id: 3 | 2

Word: 6 | Task Id: 3 | 3

Word: 6 | Task Id: 3 | 4

Word: 6 ----- Completed.

Word: 6 | Task Id: 3 | 0

Word: 6 | Task Id: 3 | 1

Word: 6 | Task Id: 3 | 2

Word: 6 | Task Id: 3 | 3

Word: 6 | Task Id: 3 | 4

Word: 6 ----- Completed.

Word: 6 | Task Id: 3 | 0

Word: 6 | Task Id: 3 | 1

Word: 6 | Task Id: 3 | 2

Word: 6 | Task Id: 3 | 3

Word: 6 | Task Id: 3 | 4

Word: 6 ----- Completed.

Word: 6 | Task Id: 3 | 0

Word: 6 | Task Id: 3 | 1

Word: 6 | Task Id: 3 | 2

Word: 6 | Task Id: 2 | 0

Word: 6 | Task Id: 2 | 1

Word: 6 | Task Id: 2 | 2

Word: 6 | Task Id: 2 | 3

Word: 6 | Task Id: 2 | 4

Word: 6 ----- Completed.

Word: 6 | Task Id: 3 | 3

Word: 6 | Task Id: 3 | 4

Word: 6 ----- Completed.

Finished.

Time Taken: 00.00:00:00

Why is every single number 6? I understand how the threading is working, but not the passing / referencing of the parameters.

So those are my two questions. Any help would be fantastic. I searched google for a while and could not find any documentation that made sense to me.

Paul Gildehaus
  • 105
  • 2
  • 9
  • 4
    You could fix the output of the numbers by adding `var x = i;` and changing the function call to use `"Word: " + x` as the parameter instead, so that's a closure issue. – Jonathon Chase Nov 28 '18 at 19:49
  • 1
    I think you're running into an issue similar to the one here: https://stackoverflow.com/questions/271440/captured-variable-in-a-loop-in-c-sharp The key takeaway is: Your lamba has a reference to a variable which is scoped outside the function itself. Your lamba is not interpreted until you invoke it and once it is it will get the value the variable has at execution time. – N0ug4t Nov 28 '18 at 19:51
  • How did "i" ever become 6, surely with that loop it should only ever be 0,1,2,3,4,5?: N0ug4t Link explains that issue pretty well – Scriven Nov 28 '18 at 19:51
  • 2
    @Scriven "i" will finally always be 6 and then the loop is exited. – Klaus Gütter Nov 28 '18 at 19:53
  • @KlausGütter Ah, I think I get it. All the actions are performed at "action => action());". At this point "i" has changed to 6 – Scriven Nov 28 '18 at 19:56
  • @Scriven yes, exactly – Klaus Gütter Nov 28 '18 at 19:58
  • Thank you all for explaining that it has to do with closure and explaining what that is. Makes total sense now! – Paul Gildehaus Nov 28 '18 at 20:18

2 Answers2

2

Regarding the first question:

what does the "action => action()" chunk of the loop do?

This is what is going to be called for each item in the actions list.

More formally it is called body of the Paraller.ForEach and it is the delegate that is invoked once per iteration. For more info, please have a look here.

Regarding the second question:

You should first capture the variable i, by using another variable inside the block of the for statement, before you use it in the lambda you create, like below:

for (int i = 0; i < 6; i++)     
{
    var j = i; 
    actions.Add(() => Function1("Word: " + j));
}

You can find a detailed explanation why you should do so here.

Christos
  • 53,228
  • 8
  • 76
  • 108
2
public void StartTest()
{
    var actions = new List<Action>();
    for (int i = 0; i < 6; i++)
    {
        // you can't pass 'i' directly to the Action here,
        // because 'i' is in the scope of where the Action is executed
        // and since the parameter of the Action is first evaluated at the 
        // execution of the Action if you were to put 'i' it checks what value
        // 'i' has at the current point in time and since the for-loop finished
        // already it is always going to be 6
        var word = $"Word: {i}";
        actions.Add(() => Function1(word));
    }

    // you could interpret the lambda 'action => action()' as
    // foreach(var action in actions)
    //     action();
    // so it essentially tells you what it's going to do for each
    // Action Item in the Action-Collection you passed as Parallel.ForEach's first parameter,
    // while 'action' represents the "current" item
    Parallel.ForEach(actions, new ParallelOptions{MaxDegreeOfParallelism = 2}, action => action());

    Console.WriteLine("Finished. \nTime Taken: " + total.ToString(@"dd\.hh\:mm\:ss"));
    Console.Read();
}


private void Function1(string word)
{
    for (int i = 0; i < 5; i++)
        Console.WriteLine(word + " |  Task Id: " + Task.CurrentId + " |   " + i);

    Console.WriteLine(word + " ----- Completed.");
}
Tobias Tengler
  • 6,848
  • 4
  • 20
  • 34