C#从循环中启动线程会抛出IndexOutOfBoundsException

时间:2011-01-24 20:08:10

标签: c# multithreading

这很奇怪,因为很明显循环条件永远不会导致异常

Thread [] threads = new Thread[threadData.Length];
for (int i = 0; i < threadData.Length; i++)
{
   threads[i]= new System.Threading.Thread(() => threadWork(threadData[i]));
   threads[i].Start();
}

它只会导致threadData [i]

的IndexOutOfBoundsException

2 个答案:

答案 0 :(得分:10)

您已捕获循环变量i,这可能导致在每个线程最终执行并从threadData检索数据时使用“i”的最后一个值。将i分配给循环中的变量并使用它,例如:

Thread [] threads = new Thread[threadData.Length];

for (int i = 0; i < threadData.Length; i++)
{
    int index = i;
    threads[i]= new System.Threading.Thread(() => threadWork(threadData[index]));
    threads[i].Start();
}

Eric Lippert在这里有一篇关于这种现象的非常好的文章:

http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx

http://blogs.msdn.com/b/ericlippert/archive/2009/11/16/closing-over-the-loop-variable-part-two.aspx

为了深入了解这种情况发生的原因,请考虑将来可能在循环结束后,线程将在某个未确定的点执行。 Start表示线程应该启动,它实际上是异步启动,即不是立即启动。鉴于此,我们可以看到传递给Thread的lambda可能在循环结束后执行得很好。那么如何引用i

简而言之,编译器将创建一个可以封装i的辅助类,然后用这个辅助类替换对i的引用。这允许lambda在循环范围之外引用i。编译魔术的一个很好的例子,但在这种情况下有一个非明显的副作用,即它捕获循环变量:

    private class LambdaHelper
    {
        public int VarI { get; set; }
    }

    private static void SomeMethod()
    {
        LambdaHelper helper = new LambdaHelper();

        Thread[] threads = new Thread[threadData.Length];

        for (helper.VarI = 0; helper.VarI < data.Length; helper.VarI++)
        {
          threads[helper.VarI] = new Thread(() => ThreadWork(data[helper.VarI]));
          threads[helper.VarI].Start();
        }
    }

我们可以看到VarI代替i。不明显的副作用是当线程执行时它们都看到共享值,即VarI。如果线程在循环结束后开始,则它们都将看到i的最大值。

修复是将i分配给循环内的临时变量,如第一个代码示例中所述。

答案 1 :(得分:8)

这是循环捕获的常见问题 - 您已捕获循环变量,因此在线程实际启动时,i是最终值,这是数组的无效索引。解决方案是在循环中创建一个新的变量 ,然后捕获它:

Thread[] threads = new Thread[threadData.Length];
for (int i = 0; i < threadData.Length; i++)
{
    int copy = i;
    threads[i]= new System.Threading.Thread(() => threadWork(threadData[copy]));
    threads[i].Start();
}

您可以在Eric Lippert的博客上阅读更多相关信息:part 1; part 2

我个人会考虑更多地使用List<T>,并尽可能使用foreach - 甚至LINQ。诚然,foreach不会解决这个问题,但IMO通常会更清晰。

这是一个如何在LINQ中完成它的示例:

List<Thread> threads = threadData.Select(x => new Thread(() => ThreadWork(x)))
                                 .ToList();
foreach (Thread thread in threads)
{
    thread.Start();
}

或者使用直接的foreach循环,随时启动每个线程:

List<Thread> threads = new List<Thread>();
foreach (var data in threadData)
{
    var dataCopy = data;
    Thread thread = new Thread(() => ThreadWork(dataCopy));
    thread.Start();
    threads.Add(thread);
}