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

dea*_*ock 4 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();
}
Run Code Online (Sandbox Code Playgroud)

它只会导致threadData [i]的IndexOutOfBoundsException

Tim*_*oyd 10

您已捕获循环变量i,这可能导致在每个线程最终执行并从中检索数据时使用"i"的最后一个值threadData.分配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();
}
Run Code Online (Sandbox Code Playgroud)

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线程应该启动的信号,它实际上是异步启动,即不是立即启动.鉴于此,我们可以看到传递给lambda的lambda Thread可以在循环结束后执行得很好.那怎么能引用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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

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


Jon*_*eet 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();
}
Run Code Online (Sandbox Code Playgroud)

您可以在Eric Lippert的博客上阅读更多相关信息:第1部分 ; 第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();
}
Run Code Online (Sandbox Code Playgroud)

或者使用直接的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);
}
Run Code Online (Sandbox Code Playgroud)