奇怪的多线程索引来自界问题



无论我使用什么:基于螺纹类或TPL任务模式。数据总是有限制的索引。从进一步的研究中,我发现计数器的价值可以是4,甚至不可能。我错过了什么?我希望您的专家意见!

通过Visual Studio测试15.8(2017(16.1(2019(,项目定位.NET框架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();
        }
    }
}

我的期望值应该通过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(的值时,它已经可以更改。(您可以查看此情况的编译器生成的代码,以及您的(。

且任务相同 - 它们是安排的,没有立即开始。

更多详细信息:

请参见此示例(使用Action代表代替Thread(和生成代码。

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

  • 在委托内部内部 - 对于每次迭代,都使用相同的实例,并在调用委托后的值会增加。使用Action,它可以按预期工作,因为它立即执行(从生成类调用方法要打印值(,然后生成类的值会增加并新开始迭代。
  • reassign在代表外部 - 创建了生成类的实例对于每一次迭代,因此没有增量。每个迭代都有独立实例和下一个迭代无法更改上一个。

在线程的情况下,唯一的区别是没有立即启动线程,它计划执行,这需要一些时间。对于第一种情况 - 当调用打印值的方法时,该值可以增加(由于所有迭代的实例(,并且您会得到意外结果。

您可以通过多次运行应用程序(对于第一种情况(进行检查 - 打印i变量时,您将不会获得相同的结果 - 有时它在不预期时会增加(因为它花了一些时间来调用Start()和实际启动。安排后的线程执行(,有时值正确(因为线程是安排的,并且在增加 Start()后几乎立即启动(。

最新更新