奇怪的多线程索引超出范围

时间:2019-06-26 06:43:51

标签: 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.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();
        }
    }
}

i的期望值应经过0 ... 3, 但是i的实际值可能等于4或在两次迭代之间保持不变。

1 个答案:

答案 0 :(得分:7)

您应在循环开始时(而不是在lambda内部)重新分配ij

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                    
    }
}

您的线程被安排执行(它不会在调用Start()之后立即启动),并且当它开始运行时,i(和j)的值可以被更改。 (您可以查看针对这种情况以及您自己的情况的编译器生成的代码。)

与任务相同-它们是计划好的,不是立即开始的。

更多详细信息:

请参见this example(使用Action委托代替Thread)和生成的代码。

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

  • 在委托内部重新分配-每次迭代都使用相同的实例,并在调用委托后增加值。使用Action可以按预期运行, 因为它立即执行(从生成的类中调用方法 打印值),然后将生成的类的值增加并新建 迭代开始。
  • 在外部委托之外重新分配-创建了所生成类的实例 每次迭代都没有增量。每次迭代都有 独立实例,下一次迭代无法更改 上一个。

对于线程而言,唯一的区别是线程不会立即启动,而是计划执行时间,这需要一些时间。对于第一种情况-调用用于打印值的方法时,该值可能已经增加(因为所有迭代的实例相同),您会得到意外的结果。

您可以通过多次运行应用程序来检查此情况(第一种情况)-打印i变量时不会得到完全相同的结果-有时在不期望的情况下会递增(因为调用花费了一些时间) Start()并在调度后实际开始执行线程),有时值是正确的(因为在增量发生之前,线程是在调用Start()之后几乎立即调度并启动的)。