0

My understanding about thread safe method parameters is: Parameters passed into a method by value are delivered as copies of the data that was given in the arguments to the method call, so they are unique to that method call and cannot be changed by any other task. Reference parameters, conversely, are susceptible to change by code running in other tasks.

With that said, It is not perfectly clear to me why the following code (without making a local copy of the loop counter) returns the same number in every thread.

static void ExampleFunc(int i) =>
            Console.WriteLine("task " + i);
for (int i = 0; i < 10; i++)
{
    int taskN = i; //local copy instead of i
    Task.Run(() => Func(i));
}

The actual output is: task 10 ten times
I get the correct output (task 1 to 10) by passing taskN instead of i.

I expected the same result since I'm passing a type value parameter.

s.demuro
  • 399
  • 2
  • 15
  • 1
    This is known as variable capture - it occurs in the context of lambda expressions. – 500 - Internal Server Error Aug 05 '19 at 13:56
  • the question is about the difference between the output that I get using taskN vs using i. As @500-InternalServerError said it might be variable capture the topic I was looking for. I'm taking readings about it. – s.demuro Aug 05 '19 at 14:00
  • https://stackoverflow.com/questions/4642665/why-does-capturing-a-mutable-struct-variable-inside-a-closure-within-a-using-sta – Renat Aug 05 '19 at 14:01
  • `I get the correct output (task 1 to 10)` Did you mean 0 to 9? The reason I ask is that this should have been your first clue. One shows 0 to 9, the other shows mainly 10. That is often a clue that you are using the loop variable _after_ the loop has effectively exited. – mjwills Aug 05 '19 at 14:05

1 Answers1

6

Parameters passed into a method by value are delivered as copies of the data that was given in the arguments to the method call,

The question is really: when does that copy happen?

It isn't when you Task.Run(...);; rather - it is when the actual lambda gets invoked by the thread-pool, i.e. when the Func(i) gets executed. The problem here is that in most cases, the thread-pool will be slower than your loop on the active thread, so those will all happen after the loop has finished, and they will all access the same captured value of i. Ultimately, what you have is:

class CaptureContext {
    public int i;
    public void Anonymous() { Func(i); }
}
...
var ctx = new CaptureContext();
for (ctx.i = 0; ctx.i < 10; ctx.i++)
{
    int taskN = ctx.i; // not used, so will probably be removed
    Task.Run(ctx.Anonymous);
}

i.e. there is only one single i, so if all the anonymous methods get invoked after the loop, the value for all of them will be: 10.

Changing the code to:

int taskN = i; //local copy instead of i
Task.Run(() => Func(taskN));

gives you very different semantics:

class CaptureContext {
    public int taskN;
    public void Anonymous() { Func(taskN);}
}
...
for (int i = 0 ; i < 10 ; i++)
{
    var ctx = new CaptureContext();
    ctx.taskN = i;
    Task.Run(ctx.Anonymous);
}

Note that we now have 10 capture context instances each with their own taskN value that will be unique per context.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900