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
相关产品推荐
相关产品推荐

