C# 奇怪的多线程索引越界问题

C# 奇怪的多线程索引越界问题,c#,multithreading,C#,Multithreading,无论我使用什么:线程类或基于TPL任务的模式。数据上总是有一个超出界限的索引。 通过进一步研究,我发现计数器I的值可以是4,这甚至不可能。 我错过了什么?我期待你的专家意见 使用Visual Studio 15.8(2017)16.1(2019)进行测试,项目目标为.NET framework 4.72 using System; using System.Collections.Generic; using System.Threading; using System.Threading.Ta

无论我使用什么:线程类或基于TPL任务的模式。数据上总是有一个超出界限的索引。 通过进一步研究,我发现计数器I的值可以是4,这甚至不可能。 我错过了什么?我期待你的专家意见

使用Visual Studio 15.8(2017)16.1(2019)进行测试,项目目标为.NET framework 4.72

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


namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            // a multi-threading search demo, omit much code for simple and clear
            // generate 0-99, total 100 elements with ascending order
            List<int> testData = new List<int>();
            for (int i = 0; i < 100; i++)
            {
                testData.Add(i);
            }
            List<int> searchFor = new List<int>() {
                    67, 0, 99,
                    23, 24, 25,
                    -1, 106
                };

            const int threadsCount = 4;

            // Test switch
            bool useThreadInsteadOfTaskTPL = true;

            if (useThreadInsteadOfTaskTPL)
            {
                // search every piece of data
                for (int j = 0; j < searchFor.Count; j++)
                {
                    Thread[] threads = new Thread[threadsCount];
                    Console.WriteLine("Search for: {0}", searchFor[j]);
                    // trying to divide the data into 4 parts, and search in parallel
                    for (int i = 0; i < threadsCount; i++)
                    {
                        Thread thread = new Thread(() => {
                            // Capture the counters to make sure no lambda pitfall
                            int counterI = i;
                            int counterJ = j;
                            Console.WriteLine("i value: {0}", counterI);
                            Console.WriteLine("j value: {0}", counterJ);
                            // your code

                        });
                        threads[i] = thread;
                        threads[i].Start();
                    }
                    for (int i = 0; i < threads.Length; i++)
                    {
                        threads[i].Join();
                    }
                    Console.WriteLine();
                }
            }
            else
            {
                for (int j = 0; j < searchFor.Count; j++)
                {
                    Task[] tasks = new Task[threadsCount];
                    Console.WriteLine("Search for: {0}", searchFor[j]);
                    // trying to divide the data into 4 parts, and search in parallel
                    for (int i = 0; i < threadsCount; i++)
                    {
                        Task task = Task.Factory.StartNew(() => {
                            // Capture the counters to make sure no lambda pitfall
                            int counterI = i;
                            int counterJ = j;
                            Console.WriteLine("i value: {0}", counterI);
                            Console.WriteLine("j value: {0}", counterJ);
                            // your code

                        }, new CancellationTokenSource().Token,
                            TaskCreationOptions.None, TaskScheduler.Default);
                        tasks[i] = task;
                    }
                    Task.WaitAll(tasks);
                    Console.WriteLine();
                }
            }
            Console.ReadKey();
        }
    }
}
使用系统;
使用System.Collections.Generic;
使用系统线程;
使用System.Threading.Tasks;
名称空间控制台EAPP1
{
班级计划
{
静态void Main(字符串[]参数)
{
//一个多线程搜索演示,省去了很多代码,简单明了
//生成0-99个元素,按升序总共生成100个元素
List testData=new List();
对于(int i=0;i<100;i++)
{
testData.Add(i);
}
List searchFor=新列表(){
67, 0, 99,
23, 24, 25,
-1, 106
};
常数int threadscont=4;
//测试开关
bool usetreadinsteadoftasktpl=true;
如果(使用线程代替tasktpl)
{
//搜索每一条数据
for(int j=0;j{
//捕获计数器以确保没有lambda陷阱
int counterI=i;
int计数器j=j;
WriteLine(“i值:{0}”,counterI);
WriteLine(“j值:{0}”,counterJ);
//你的代码
});
螺纹[i]=螺纹;
线程[i].Start();
}
对于(int i=0;i{
//捕获计数器以确保没有lambda陷阱
int counterI=i;
int计数器j=j;
WriteLine(“i值:{0}”,counterI);
WriteLine(“j值:{0}”,counterJ);
//你的代码
},新的CancellationTokenSource()。令牌,
TaskCreationOptions.None,TaskScheduler.Default);
任务[i]=任务;
}
Task.WaitAll(任务);
Console.WriteLine();
}
}
Console.ReadKey();
}
}
}
i的期望值应该经过0…3,
但i的实际值可能等于4或在迭代之间保持不变。

在循环开始时(不在lambda内),应重新分配
i
j

for(int i=0;i
{                                
WriteLine(“i值:{0}”,counterI);
WriteLine(“j值:{0}”,counterJ);
//你的代码
}
}
您的线程计划执行(在调用
Start()
后不会立即启动),并且当它开始运行时,
i
(和
j
)的值可能已经更改。(您可以查看编译器生成的代码,了解本例和您的代码)

任务也是如此——它们是计划好的,不是立即启动的

更多详情:

请参阅(
操作
使用委托而不是
线程
)和生成的代码

您可以看到差异(生成的代码创建类的实例) 存储要打印的值和实际打印的方法):

  • 在委托内部重新分配-对于每次迭代,使用相同的实例,并在调用委托后增加值。使用
    操作
    时,它会按预期工作, 因为它立即执行(从生成的类调用方法 若要打印值),则生成的类的值将递增并新建 迭代开始了
  • 重新分配外部委托-已创建生成类的实例 对于每个迭代,没有增量。每个迭代都有 独立实例和下一次迭代无法更改的值 上一个
对于线程,唯一的区别是线程不会立即启动,它会被安排执行,这需要一些时间。对于第一种情况,当调用打印值的方法时,该值可能已经递增(因为所有迭代都有相同的实例),您会得到意外的结果

您可以通过多次运行应用程序来检查这一点(对于第一种情况)-在打印
i
variable-一些
for (int i = 0; i < threadsCount; i++)
{
    // Capture the counters to make sure no lambda pitfall
    int counterI = i;
    int counterJ = j;

    Thread thread = new Thread(() =>
    {                                
        Console.WriteLine("i value: {0}", counterI);
        Console.WriteLine("j value: {0}", counterJ);

        // your code                    
    }
}