任务并行库和循环

时间:2011-08-12 12:29:44

标签: c#-4.0 task-parallel-library

我遇到的情况是,我正在创建的任务在我调试代码时似乎只能工作。

正如您将在下面看到的那样,我继续得到一个超出范围异常的索引,这是不可能的,因为循环永远不会达到5。

如果我在循环中添加一个断点,然后继续按F10直到程序结束,所有都按预期工作。

任何想法我做错了什么?

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
class Program
{        
    static void Main(string[] args)
    {
        int[] numbers = new int[5] { 1, 2, 3, 4, 5 };
        List<int> nums = new List<int>();

        var tasks = new Task[5];

        for (int i = 0; i < numbers.Length; i++)
        {
            tasks[i] = Task.Factory.StartNew(() =>
                {
                    nums.Add(numbers[i]);
                }, 
                TaskCreationOptions.None);
        }
        Task.WaitAll(tasks);

        for (int i = 0; i < nums.Count; i++)
        {
            Console.WriteLine(nums[i]);
        }
        Console.ReadLine();
    }
}

}

我希望看到1,2,3,4,5,但不能以任何特定顺序运行async

奇怪的是,这确实有效,但我没有看到除了额外打字之外的其他区别。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
class Program
{        
    static void Main(string[] args)
    {
        int[] numbers = new int[5] { 1, 2, 3, 4, 5 };
        List<int> nums = new List<int>();

        var tasks = new Task[5]
        {
            Task.Factory.StartNew(() => {nums.Add(numbers[0]);}, TaskCreationOptions.None),
            Task.Factory.StartNew(() => {nums.Add(numbers[1]);}, TaskCreationOptions.None),
            Task.Factory.StartNew(() => {nums.Add(numbers[2]);}, TaskCreationOptions.None),
            Task.Factory.StartNew(() => {nums.Add(numbers[3]);}, TaskCreationOptions.None),
            Task.Factory.StartNew(() => {nums.Add(numbers[4]);}, TaskCreationOptions.None)
        };

        Task.WaitAll(tasks);

        for (int i = 0; i < nums.Count; i++)
        {
            Console.WriteLine(nums[i]);
        }
        Console.ReadLine();
    }
}

}

由于

麦克

3 个答案:

答案 0 :(得分:3)

由于任务发生在循环之外,在任务执行时已经完成,所以此时我是5(循环退出条件)。

在你的第二个例子中,你不使用i,而是对值进行硬编码,以便问题消失。

在调试器中,时间不同,任务可以在循环完成之前执行:同样,问题消失了。

我认为你可以这样解决:

var ii=i;
tasks[i] = Task.Factory.StartNew(() =>
                {
                    nums.Add(numbers[ii]);
                }, 
                TaskCreationOptions.None);

答案 1 :(得分:3)

更新:在C#5中,这不再是一个问题。 breaking change was使得foreach的循环变量在逻辑上位于循环内。

这是您可以在C#代码中犯下的最常见错误之一的另一个示例:在匿名委托中捕获循环变量。 Eric Lippert有一个good explanation出现了什么问题。

实际上,你的情况更糟,因为理论上它可能是随机的对错。假设您进行了以下更改:

for (int i = 0; i < numbers.Length; i++)
{
    tasks[i] = ...;
    tasks[i].Wait();
}

然后你的程序突然按预期工作。原因是,当任务执行时,您的任务将获取i(循环变量)的值,而不是创建该任务时i的值。因此,原始程序中可以使用以下顺序:

i = 0
Create task 0
i = 1
Run task 0: This task sees i = 1
Create task 1
i = 2
Create task 2
i = 3
Create task 3
i = 4
Create task 4
i = 5
Run task 1: This task sees i = 5 and throws an exception

Tom的回答通过在循环中引入一个新的变量ii来解决这个问题。创建每个任务时,它会捕获此变量,该变量具有每次迭代的固定值。

答案 2 :(得分:2)

这里的问题是Task访问i变量,并且当任务运行时,这个变量将被改变,并且你在最后一次迭代中得到异常的原因是变量i将是5,并且虽然循环将在此增量后停止,但任务主体仍然引用它,所以此行将抛出

nums.Add(numbers[i]);

因为显然5超出了范围值。

要解决此问题,您需要将i作为状态参数传递给StartNew方法

 tasks[i] = Task.Factory.StartNew((obj) =>
            {
                int index = (int) obj;
                nums.Add(numbers[index]);
            }, 
            i);