【问题标题】:For loop goes out of rangeFor 循环超出范围
【发布时间】:2011-09-07 17:38:20
【问题描述】:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();
            myClass.StartTasks();
        }
    }
    class MyClass
    {
        int[] arr;
        public void StartTasks()
        {
            arr = new int[2];
            arr[0] = 100;
            arr[1] = 101;

            for (int i = 0; i < 2; i++)
            {
                Task.Factory.StartNew(() => WorkerMethod(arr[i])); // IndexOutOfRangeException: i==2!!!
            }
        }

        void WorkerMethod(int i)
        {
        }
    }
}

似乎 i++ 在循环迭代完成之前又执行了一次。为什么会出现 IndexOutOfRangeException?

【问题讨论】:

  • +1:这很有趣!很棒的一段代码,复制粘贴,我可以自己测试。
  • 显然微软也注意到了,这种行为很愚蠢,他们修复了它。在 C# 5.0 中,上述代码将按预期工作:-D
  • 很高兴有人以前严重遇到过这个问题......只是觉得我完全愚蠢,for循环超出范围哈哈。赞成。非常有用的问题。

标签: c# for-loop


【解决方案1】:

使用foreach 不会抛出:

foreach (var i in arr)
{
  Task.Factory.StartNew(() => WorkerMethod(i));
}

但它也不起作用:

101
101

它使用数组中的最后一个条目执行WorkerMethod。为什么在其他答案中得到了很好的解释。

确实工作:

Parallel.ForEach(arr, 
                 item => Task.Factory.StartNew(() => WorkerMethod(item))
                 );

注意

这实际上是我第一次亲身体验System.Threading.Tasks。我发现这个问题,我天真的答案,尤其是其他一些对我个人学习经验有用的答案。我会在这里留下我的答案,因为它可能对其他人有用。

【讨论】:

  • 它可以工作,因为你没有arr[i]。当WorkerMethod 尝试访问arr[2] 值时发生异常
  • 再次查看WorkerMethod() 接收的值。所有任务都将收到集合的最后一个值。它会工作(如 not throw),但不会按预期工作。
  • @Dyppl - 正如杰夫指出的那样,实际上它甚至不起作用。但它不会抛出 - 欢呼:-(
【解决方案2】:

您正在关闭循环变量。当需要调用WorkerMethod 时,i 的值可以是 2,而不是 0 或 1。

当您使用闭包时,重要的是要了解您现在使用的不是变量的值,而是变量本身。因此,如果您像这样在循环中创建 lambda:

for(int i = 0; i < 2; i++) {
    actions[i] = () => { Console.WriteLine(i) };
}

稍后执行动作,它们都会打印“2”,因为这就是i的值。

在循环中引入局部变量将解决您的问题:

for (int i = 0; i < 2; i++)
{
    int index = i;
    Task.Factory.StartNew(() => WorkerMethod(arr[index])); 
}

这是尝试Resharper 的又一理由 - 它提供了许多警告,可帮助您及早发现此类错误。 “关闭循环变量”就在其中plug>

【讨论】:

  • +1 这个答案比说明 "... 因为任务可以同时执行值循环变量的值可能与您启动任务时的值不同”
  • @bottlenecked:好吧,在这种情况下,它仍然与并发性有关,因为StartNew 实际上是从那里开始任务的,所以 可能 闭包会及时评估.如果你在循环中填充Sleep(1000),你应该没问题。但我仍然认为这是误解闭包而不是并发的问题
  • +1;至少对于无耻的插件而言:R# 确实警告“访问修改后的闭包”。请注意,如果出现 forforeach 循环,它会发出警告。
  • 在阅读了大量 MSDN 文档之后。看来,它不是一个错误 - 它是一个功能。整个 lambda 表达式在循环退出之后执行,这很令人困惑并且会产生类似上面的结果。我想完全避免使用 lambda,但 MSDN 现在在示例中到处都使用它。是否有另一种方法可以使用任意输入(例如 int、string 和 char)启动线程,而无需创建单独的类并将其作为对象传递?我想尽可能避免运行时类型检查。
  • @user782534:这不是整个 lambda 表达式的问题,而是使用闭包的问题。闭包允许您使用声明 lambda 表达式的上下文中的变量。它通常非常方便,您只需要小心。 Lambda 表达式和闭包正是出于这个原因:因为所有其他方式都需要您编写大量管道,例如创建类等。与其避免 lambda 和闭包,不如确保您完全理解它,从而成为您的朋友。 csharpindepth.com/Articles/Chapter5/Closures.aspx 是一篇好文章。
【解决方案3】:

原因是您在并行任务中使用了循环变量。因为任务可以并发执行,所以循环变量的值可能与启动任务时的值不同。

您在循环中启动了任务。当任务开始查询循环变量时,循环已经结束,因为变量 i 现在超出了停止点。

即:

  • i = 2,循环退出。
  • 任务使用变量 i(现在是 2)

您应该使用 Parallel.For 来并行执行循环体。这里是an example of how to use Parallel.For

另外,如果您想保持当前结构,可以将 i 的副本复制到循环局部变量中,并且循环局部副本会将其值保留到并行任务中。

例如

for (int i = 0; i < 2; i++)
{
  int localIndex = i;
  Task.Factory.StartNew(() => WorkerMethod(arr[localIndex])); 
} 

【讨论】:

  • 这里还有一些required reading
  • 还有一个不错的大 ParallelProgramsinNET4_CodingGuidelines.pdf 来自 MS; download.microsoft.com/download/B/C/F/…
  • 小后续问题:为什么foreach 不抛出?纯属偶然?
  • @Marijn:请参阅我对foreach 不引发异常的回答的评论
  • 听起来像是经典的闭包问题
猜你喜欢
  • 2019-04-17
  • 1970-01-01
  • 2013-09-28
  • 2013-11-25
  • 2017-08-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-13
相关资源
最近更新 更多