You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#循环调用静态多线程方法时List参数传递值错误问题排查

问题原因

这个问题是两个C#核心特性共同导致的:

  • List<T>是引用类型,你在循环外部只创建了1个registers列表实例,每次循环调用registers.Clear()只是清空这个实例的内部内容,并没有生成新的列表对象。后续所有AddMaster调用拿到的都是指向同一个堆内存列表的引用,只要这个列表的内容被修改,所有指向它的引用读到的值都会变。
  • AddMaster里的异步线程/Task是滞后执行的:你在AddMaster入口写日志是同步执行的,调用当下列表里的内容还没被后续循环修改,所以日志符合预期;但Lambda闭包捕获的是registers变量本身(不是调用瞬间的值快照),等后台线程真正调度执行到SetRegisters时,主线程的循环大概率已经跑完,那个唯一的registers列表里存的已经是最后一次循环写入的内容,所以两次调用读到的都是最后一次的值。
    补充:你看到IP地址打印正常,是因为string是不可变类型,每次循环赋值都会生成新的字符串实例,不会出现后续修改覆盖之前值的情况。
修复方案

核心是切断不同次调用之间的引用共享,配合防御性编程避免同类问题复发:

  • 修改循环逻辑,不要复用同一个列表实例
    删掉循环开头的registers.Clear(),改为在每次循环内部新建独立的List<RegisterMap>对象,保证每次传给AddMaster的都是独立的列表引用,后续循环的修改不会影响之前传出去的列表:
    foreach (var equipment in MSSQL.GetEquipments(true)) 
    {
        // 每次循环新建列表,不要复用外部实例
        var registers = new List<RegisterMap>();
        // 其余原有逻辑不变
        System.IO.File.AppendAllText("dataeq.txt", equipment.IPAddress + "    " + equipment.Name + "     " + equipment.ID +  "\n");
        try
        {
            registers.Add(new RegisterMap
            {
                FriendlyName = equipment.IPAddress + ":Value 1",
                Register = 2001,
                Type = RegisterType.HoldingRegister,
                StationID = 1
            });
            registers.Add(new RegisterMap
            {
                FriendlyName = equipment.IPAddress + ":Value 2",
                Register = 2002,
                Type = RegisterType.HoldingRegister,
                StationID = 1
            });
            A.AddMaster(equipment.IPAddress, 502, registers);
            var json = new JavaScriptSerializer().Serialize(registers);
            System.IO.File.AppendAllText("data_reg.txt", json + "\n\n");
        }
        catch(Exception err)
        {
            System.Windows.MessageBox.Show(err.Message);
        }
    }
    
  • 异步逻辑前做参数快照,避免闭包捕获可变变量
    在AddMaster启动异步线程前,先把传入的参数拷贝一份到本地不可变变量,Lambda只捕获这个固定的本地变量,不要直接捕获可能被修改的参数:
    public static void AddMaster(string ipAddress, int port, List<RegisterMap> registers)
    {
        System.IO.File.AppendAllText("datalog_MB.txt", ipAddress + "   " + registers[0].FriendlyName + "\n");
        // 提前做参数快照
        var snapshotIp = ipAddress;
        var snapshotPort = port;
        var snapshotRegisters = new List<RegisterMap>(registers);
        
        new Thread(() =>
        {
            _tasks.Add(Task.Factory.StartNew(() =>
            {
                Monitor.Enter(_masters);
                // 传入快照值,不直接用原参数
                _masters.Add(new Master().Connect(snapshotIp, snapshotPort).SetRegisters(snapshotRegisters));
                _masters.Last().OnEvent += MasterEvent;
                Debug.WriteLine(_masters.Count + " TCP masters connected");
                Monitor.Exit(_masters);
            }));
        }).Start();
    }
    
  • 内部存储时做列表副本,做防御性编程
    SetRegisters方法不要直接保存外部传入的列表引用,而是创建副本存在内部,避免外部代码修改列表影响内部状态:
    public Master SetRegisters(List<RegisterMap> registerList)
    {
        System.IO.File.AppendAllText("datalog_MB_1.txt", _hostname + "   " + registerList[0].FriendlyName + "\n");
        // 新建列表副本存储,不持有外部引用
        _registersToRead = new List<RegisterMap>(registerList);
        return this;
    }
    

以上三点中,第一点是核心修复,后两点是防御性优化,避免后续其他业务场景再出现同类引用篡改、闭包捕获问题。


内容的提问来源于stack exchange,提问作者mrid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 00:57:19