13

got a Problem, hope someone can help me out.

i try to start 4 Task in an Loop but im getting an ArgumentOutOfRangeException:

 for (int i = 0; i < 4; i++)
     {
          //start task with current connection
          tasks[i] = Task<byte[]>.Run(() => GetData(i, plcPool[i]));
     }

The Loop gets an Overflow because i = 4

if i start the Tasks without a Loop, they run without any Problems:

            tasks[0] = Task<byte[]>.Run(() => GetData(0, plcPool[0]));
            tasks[1] = Task<byte[]>.Run(() => GetData(1, plcPool[1]));
            tasks[2] = Task<byte[]>.Run(() => GetData(2, plcPool[2]));
            tasks[3] = Task<byte[]>.Run(() => GetData(3, plcPool[3]));

dont know why? The Tasks GetData from a Siemens PLC via Socket Connection. The PLC Supports up to 32 Connections. I receive 200 Bytes per Connection.

 private byte[] GetData(int id, PLC plc)
    {
        switch (id)
        {
            case 0:
                return plc.ReadBytes(DataType.DataBlock, 50, 0, 200);
            case 1:
                return plc.ReadBytes(DataType.DataBlock, 50, 200, 200);
            case 2:
                return plc.ReadBytes(DataType.DataBlock, 50, 500, 200);
            case 3:
                return plc.ReadBytes(DataType.DataBlock, 50, 700, 200);
            case 4:
                return plc.ReadBytes(DataType.DataBlock, 50, 900, 117);
            default:
                return null;
        }
    }

any idea?

Regards Sam

Sam
  • 291
  • 3
  • 9

3 Answers3

19

It's probably caused by a closure problem.

Try this:

 for (int i = 0; i < 4; i++)
 {
      //start task with current connection
      int index = i;
      tasks[index] = Task<byte[]>.Run(() => GetData(index, plcPool[index]));
 }

What is probably happening is that when the last thread starts running, the loop has already incremented i to 4, and that's the value that gets passed to GetData(). Capturing the value of i into a separate variable index and using that instead should solve that issue.

As an example, if you try this code:

public static void Main()
{
    Console.WriteLine("Starting.");

    for (int i = 0; i < 4; ++i)
        Task.Run(() => Console.WriteLine(i));

    Console.WriteLine("Finished. Press <ENTER> to exit.");
    Console.ReadLine();
}

it will often give you this kind of output:

Starting.
Finished. Press <ENTER> to exit.
4
4
4
4

Change that code to:

public static void Main()
{
    Console.WriteLine("Starting.");

    for (int i = 0; i < 4; ++i)
    {
        int j = i;
        Task.Run(() => Console.WriteLine(j));
    }

    Console.WriteLine("Finished. Press <ENTER> to exit.");
    Console.ReadLine();
}

and you get something like

Starting.
Finished. Press <ENTER> to exit.
0
1
3
2

Note how it is STILL NOT NECESSARILY IN ORDER! You will see all the correct values printed out, but in an indeterminate order. Multithreading is tricksy!

Daniel B
  • 3,109
  • 2
  • 33
  • 42
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • I dont see where is changing `i`? also its passed to `GetData` so that shouldnt be the cause. i think closure problem only happens when value is changed inside function. – M.kazem Akhgary Oct 22 '15 at 07:57
  • 3
    @M.kazemAkhgary `i` is being changed by the loop. – Matthew Watson Oct 22 '15 at 07:58
  • wow. yeah thats it. because it is not waiting for the Task so in last iteration it causes overflow, good point – M.kazem Akhgary Oct 22 '15 at 08:00
  • Correct this is a closure issue, since Task contains value modified in the For loop, they do not sync up, Task will start using the value, which is available when it start running not the one that is assigned, this is a standard closure issue – Mrinal Kamboj Oct 22 '15 at 08:03
  • Check more details on closure, in following article by Jon Skeet, http://csharpindepth.com/Articles/Chapter5/Closures.aspx – Mrinal Kamboj Oct 22 '15 at 08:06
1

In 2021, You should really just use the built in Parallel.For for this. It's quite simple to achieve your desired effect:

ConcurrentBag<byte[]> results = new ConcurrentBag<byte[]>();

ParallelLoopResult result = Parallel.For(0, 4, (i, state) => {
   results.Add(GetData(i, plcPool[i]));
});
Liam
  • 27,717
  • 28
  • 128
  • 190
-1

In C# 5, the loop variable of a foreach will be logically inside the loop, and therefore closures will close over a fresh copy of the variable each time. The "for" loop will not be changed.

Another way is that:

  1. Create tasks
  2. Run the tasks.
  3. Wait all task to finish.

The demo code as follow:

 internal class Program
{
    private static void Main(string[] args)
    {
        Task[] tasks = new Task[4];

        for (int i = 0; i < 4; i++)
        {
            //start task with current connection
            tasks[i] = new Task<byte[]>(GetData,i);
        }

        foreach (var task in tasks)
        {
            task.Start();
        }

        Task.WaitAll(tasks);

        Console.Read();
    }

    private static byte[] GetData(object index)
    {
        var i = (int) index;
        switch (i)
        {
            case 0:

                //return plc.ReadBytes(DataType.DataBlock, 50, 0, 200);
                Console.WriteLine(i);
                return new byte[] { };
            case 1:
            //return plc.ReadBytes(DataType.DataBlock, 50, 200, 200);
            case 2:
                Console.WriteLine(i);
                return new byte[] { };
            //return plc.ReadBytes(DataType.DataBlock, 50, 500, 200);
            case 3:
                Console.WriteLine(i);
                return new byte[] { };
            //return plc.ReadBytes(DataType.DataBlock, 50, 700, 200);
            case 4:
                //return plc.ReadBytes(DataType.DataBlock, 50, 900, 117);
                Console.WriteLine(i);
                return new byte[] { };

            default:
                return null;
        }
    }
}

OUTPUT:

    3  
    1  
    0  
    2  

Note: it's new Task<byte[]>(GetData,i); not new Task<byte[]>(()=>GetData(i));

()=>v means "return the current value of variable v", not "return the value v was back when the delegate was created". Closures close over variables, not over values.

So the new Task<byte[]>(GetData,i); have no " Closures Problem "

huoxudong125
  • 1,966
  • 2
  • 26
  • 42
  • That has the same problem. Try it with `() => Console.WriteLine(i)` as per my example and you'll see it go wrong. (Also if you have Resharper, it will warn you about a "modified closure".) – Matthew Watson Oct 22 '15 at 08:19
  • @MatthewWatson did you test? I have tested it with vs2015. – huoxudong125 Oct 22 '15 at 08:21
  • It would be still incorrect since you are passing **i** as part of task, which will cause the same closure issue – Mrinal Kamboj Oct 22 '15 at 08:23
  • @huoxudong125 Yep I certainly tested. It's definitely not going to work for `Console.WriteLine()`. But you should not declare `index` as `object`. That causes a copy to be made, which is why it appears to work for you. Change it to `int` like it should be and try again. You need to use `tasks[i] = new Task(() => GetData(i));` – Matthew Watson Oct 22 '15 at 08:23
  • @MatthewWatson `tasks[i] = new Task(GetData,i);` not `tasks[i] = new Task(()=>GetData(i));` – huoxudong125 Oct 22 '15 at 08:27
  • @MatthewWatson I just give an idea. it is not an full solution. – huoxudong125 Oct 22 '15 at 08:30
  • 1
    Ok, just be aware that the reason it works is because you are effectively making a copy of the loop variable `i` by passing it to a method which expects an `object`. The value of `i` is therefore being boxed, which makes a copy at that point. That it works has NOTHING to do with you starting the threads later! – Matthew Watson Oct 22 '15 at 08:38
  • @MatthewWatson I don't thinks so.it's really reason is that `() => GetData(i)` is anonymous methods or lambda expressions.not because of `index` is a `object`. – huoxudong125 Oct 22 '15 at 08:42
  • 1
    @huoxudong125 Sorry, but you're wrong. All the overloads of the `Task` constructor use an `Action` delegate, [including the one you're using](https://msdn.microsoft.com/en-us/library/dd235693%28v=vs.110%29.aspx). It's important that other people understand this, even if you don't want to. – Matthew Watson Oct 22 '15 at 09:23
  • Note that if you replace your code in the loop with this: `Task.Factory.StartNew(GetData, i);` then the code will STILL work. Like I said, the reason it works is not because you are starting the tasks outside the loop. It's because you're making a copy of `i` by passing it as an `object`. – Matthew Watson Oct 22 '15 at 09:29
  • @MatthewWatson what's the difference between `Task.Factory.StartNew(GetData, i);` and ` tasks[i] = Task.Factory.StartNew(()=> GetData(i));` the first task will have the i ,when the `for` loop. but the second one don't get the i immediately unitl the `task.Run()`. **so the second one has `closure` problem,but first one don't have the problem. I think you must know when there is a `closure problem`.** In another way the copy of `i` rely on the custructor of `Task` class,is not `index` of `GetData()`. – huoxudong125 Oct 23 '15 at 00:35
  • @MatthewWatson I want to say that ,`GetData(object i)` the parameter `i` is `object` class just because the `Task` 's Constructor. if `Task` have a constructor with a `int` parameter ,I will use it first,not use the `object` one. so that's not care of performance ,I just use the Task's default constructor.If you say that if there is a boxing ,it's not a good code. I think [ParameterizedThreadStart](https://msdn.microsoft.com/en-us/library/system.threading.parameterizedthreadstart(v=vs.110).aspx) is also a bad code. – huoxudong125 Oct 23 '15 at 00:47
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/93123/discussion-between-huoxudong125-and-matthew-watson). – huoxudong125 Oct 23 '15 at 03:14