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

标签 c# multithreading

无论我使用什么:线程类或基于 TPL 任务的模式。数据上总是存在超出范围的索引。 通过进一步的研究,我发现 counter 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 或在迭代之间保持不变。

最佳答案

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

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 委托(delegate)代替 Thread)和生成的代码。

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

  • 在委托(delegate)内部重新分配 - 每次迭代都使用相同的实例,并且在调用委托(delegate)后增加值。使用 Action 它按预期工作, 因为它会立即执行(从生成的类调用方法 打印值),然后生成类的值递增和新的 迭代开始。
  • reassign outside delegate - 创建生成类的实例 对于每次迭代,因此没有增量。每次迭代都有 独立实例和下一次迭代不能改变值 上一个。

在线程的情况下,唯一的区别是线程不会立即启动,它会被安排执行,这需要一些时间。对于第一种情况 - 当调用打印值的方法时,该值可能已经递增(因为所有迭代的实例相同)并且您会得到意想不到的结果。

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

关于c# - 奇怪的多线程索引越界问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56766689/

相关文章:

c++ - 为什么我在这个程序上得到 "abort"?

c# - 在显式结构上出现 "incorrectly aligned or overlapped by non-object"错误

c# - 从范围内看不到标签匹配 'AutofacWebRequest' 的范围

java - 通过telnet连接esp8266 android

Delphi:线程作业的线程列表 - 排队

java - 对并行线程组执行顺序操作

c# - .NET CF 内存不足异常

c# - 如何从 ASP.NET Core 中的 Program.cs 访问 IWebHostEnvironment

带有 HttpWebRequest 的 C# 程序只能通过 Fiddler 工作

c++ - Tinyxml 多任务